3395 lines
		
	
	
		
			104 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			3395 lines
		
	
	
		
			104 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* eslint-env browser, jquery */
 | |
| /* global CodeMirror, Cookies, moment, editor, ui, Spinner,
 | |
|    modeType, Idle, serverurl, key, gapi, Dropbox, FilePicker
 | |
|    ot, MediaUploader, hex2rgb, num_loaded, Visibility */
 | |
| 
 | |
| 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')
 | |
| 
 | |
| import toMarkdown from 'to-markdown'
 | |
| 
 | |
| import { saveAs } from 'file-saver'
 | |
| import randomColor from 'randomcolor'
 | |
| 
 | |
| import _ from 'lodash'
 | |
| 
 | |
| import List from 'list.js'
 | |
| 
 | |
| import {
 | |
|     checkLoginStateChanged,
 | |
|     setloginStateChangeEvent
 | |
| } from './lib/common/login'
 | |
| 
 | |
| import {
 | |
|     debug,
 | |
|     DROPBOX_APP_KEY,
 | |
|     GOOGLE_API_KEY,
 | |
|     GOOGLE_CLIENT_ID,
 | |
|     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 './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/editor/modeType'
 | |
| 
 | |
| var defaultTextHeight = 20
 | |
| var viewportMargin = 20
 | |
| var defaultEditorMode = 'gfm'
 | |
| 
 | |
| var idleTime = 300000 // 5 mins
 | |
| var updateViewDebounce = 100
 | |
| var cursorMenuThrottle = 50
 | |
| var cursorActivityDebounce = 50
 | |
| var cursorAnimatePeriod = 100
 | |
| var supportContainers = ['success', 'info', 'warning', 'danger']
 | |
| var supportCodeModes = ['javascript', 'typescript', 'jsx', 'htmlmixed', 'htmlembedded', 'css', 'xml', 'clike', 'clojure', 'ruby', 'python', 'shell', 'php', 'sql', 'haskell', 'coffeescript', 'yaml', 'pug', 'lua', 'cmake', 'nginx', 'perl', 'sass', 'r', 'dockerfile', 'tiddlywiki', 'mediawiki', 'go', 'gherkin']
 | |
| var supportCharts = ['sequence', 'flow', 'graphviz', 'mermaid', 'abc']
 | |
| var supportHeaders = [
 | |
|   {
 | |
|     text: '# h1',
 | |
|     search: '#'
 | |
|   },
 | |
|   {
 | |
|     text: '## h2',
 | |
|     search: '##'
 | |
|   },
 | |
|   {
 | |
|     text: '### h3',
 | |
|     search: '###'
 | |
|   },
 | |
|   {
 | |
|     text: '#### h4',
 | |
|     search: '####'
 | |
|   },
 | |
|   {
 | |
|     text: '##### h5',
 | |
|     search: '#####'
 | |
|   },
 | |
|   {
 | |
|     text: '###### h6',
 | |
|     search: '######'
 | |
|   },
 | |
|   {
 | |
|     text: '###### tags: `example`',
 | |
|     search: '###### tags:'
 | |
|   }
 | |
| ]
 | |
| 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 () {
 | |
|       var 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'
 | |
|   }
 | |
| }
 | |
| const defaultMode = modeType.view
 | |
| 
 | |
| // 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
 | |
| window.currentMode = defaultMode
 | |
| let currentStatus = statusType.offline
 | |
| let 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()
 | |
| var editor = editorInstance.init(textit)
 | |
| 
 | |
| // FIXME: global referncing in jquery-textcomplete patch
 | |
| window.editor = editor
 | |
| 
 | |
| var inlineAttach = inlineAttachment.editors.codemirror4.attach(editor)
 | |
| defaultTextHeight = parseInt($('.CodeMirror').css('line-height'))
 | |
| 
 | |
| //  initalize ui reference
 | |
| const ui = getUIElements()
 | |
| 
 | |
| // page actions
 | |
| var opts = {
 | |
|   lines: 11, // The number of lines to draw
 | |
|   length: 20, // The length of each line
 | |
|   width: 2, // The line thickness
 | |
|   radius: 30, // The radius of the inner circle
 | |
|   corners: 0, // Corner roundness (0..1)
 | |
|   rotate: 0, // The rotation offset
 | |
|   direction: 1, // 1: clockwise, -1: counterclockwise
 | |
|   color: '#000', // #rgb or #rrggbb or array of colors
 | |
|   speed: 1.1, // Rounds per second
 | |
|   trail: 60, // Afterglow percentage
 | |
|   shadow: false, // Whether to render a shadow
 | |
|   hwaccel: true, // Whether to use hardware acceleration
 | |
|   className: 'spinner', // The CSS class to assign to the spinner
 | |
|   zIndex: 2e9, // The z-index (defaults to 2000000000)
 | |
|   top: '50%', // Top position relative to parent
 | |
|   left: '50%' // Left position relative to parent
 | |
| }
 | |
| 
 | |
| /* eslint-disable no-unused-vars */
 | |
| var spinner = new Spinner(opts).spin(ui.spinner[0])
 | |
| /* eslint-enable no-unused-vars */
 | |
| 
 | |
| // idle
 | |
| var idle = new Idle({
 | |
|   onAway: function () {
 | |
|     idle.isAway = true
 | |
|     emitUserStatus()
 | |
|     updateOnlineStatus()
 | |
|   },
 | |
|   onAwayBack: function () {
 | |
|     idle.isAway = false
 | |
|     emitUserStatus()
 | |
|     updateOnlineStatus()
 | |
|     setHaveUnreadChanges(false)
 | |
|     updateTitleReminder()
 | |
|   },
 | |
|   awayTimeout: idleTime
 | |
| })
 | |
| ui.area.codemirror.on('touchstart', function () {
 | |
|   idle.onActive()
 | |
| })
 | |
| 
 | |
| var haveUnreadChanges = false
 | |
| 
 | |
| function setHaveUnreadChanges (bool) {
 | |
|   if (!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
 | |
| var wasFocus = false
 | |
| Visibility.change(function (e, state) {
 | |
|   var hidden = Visibility.hidden()
 | |
|   if (hidden) {
 | |
|     if (editorHasFocus()) {
 | |
|       wasFocus = true
 | |
|       editor.getInputField().blur()
 | |
|     }
 | |
|   } else {
 | |
|     if (wasFocus) {
 | |
|       if (!visibleXS) {
 | |
|         editor.focus()
 | |
|         editor.refresh()
 | |
|       }
 | |
|       wasFocus = false
 | |
|     }
 | |
|     setHaveUnreadChanges(false)
 | |
|   }
 | |
|   updateTitleReminder()
 | |
| })
 | |
| 
 | |
| // when page ready
 | |
| $(document).ready(function () {
 | |
|   idle.checkAway()
 | |
|   checkResponsive()
 | |
|     // if in smaller screen, we don't need advanced scrollbar
 | |
|   var scrollbarStyle
 | |
|   if (visibleXS) {
 | |
|     scrollbarStyle = 'native'
 | |
|   } else {
 | |
|     scrollbarStyle = 'overlay'
 | |
|   }
 | |
|   if (scrollbarStyle !== editor.getOption('scrollbarStyle')) {
 | |
|     editor.setOption('scrollbarStyle', scrollbarStyle)
 | |
|     clearMap()
 | |
|   }
 | |
|   checkEditorStyle()
 | |
|   /* we need this only on touch devices */
 | |
|   if (isTouchDevice) {
 | |
|     /* cache dom references */
 | |
|     var $body = $('body')
 | |
| 
 | |
|     /* bind events */
 | |
|     $(document)
 | |
|     .on('focus', 'textarea, input', function () {
 | |
|       $body.addClass('fixfixed')
 | |
|     })
 | |
|     .on('blur', 'textarea, input', function () {
 | |
|       $body.removeClass('fixfixed')
 | |
|     })
 | |
|   }
 | |
|   // showup
 | |
|   $().showUp('.navbar', {
 | |
|     upClass: 'navbar-hide',
 | |
|     downClass: 'navbar-show'
 | |
|   })
 | |
|   // tooltip
 | |
|   $('[data-toggle="tooltip"]').tooltip()
 | |
|   // shortcuts
 | |
|   // allow on all tags
 | |
|   key.filter = function (e) { return true }
 | |
|   key('ctrl+alt+e', function (e) {
 | |
|     changeMode(modeType.edit)
 | |
|   })
 | |
|   key('ctrl+alt+v', function (e) {
 | |
|     changeMode(modeType.view)
 | |
|   })
 | |
|   key('ctrl+alt+b', function (e) {
 | |
|     changeMode(modeType.both)
 | |
|   })
 | |
|   // 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)
 | |
| 
 | |
| function autoSyncscroll () {
 | |
|   if (editorHasFocus()) {
 | |
|     syncScrollToView()
 | |
|   } else {
 | |
|     syncScrollToEdit()
 | |
|   }
 | |
| }
 | |
| 
 | |
| var windowResizeDebounce = 200
 | |
| var windowResize = _.debounce(windowResizeInner, windowResizeDebounce)
 | |
| 
 | |
| function windowResizeInner (callback) {
 | |
|   checkLayout()
 | |
|   checkResponsive()
 | |
|   checkEditorStyle()
 | |
|   checkTocStyle()
 | |
|   checkCursorMenu()
 | |
|   // refresh editor
 | |
|   if (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 (var i = 0; i < onlineUsers.length; i++) {
 | |
|           if (onlineUsers[i].id !== personalInfo.id) { buildCursor(onlineUsers[i]) }
 | |
|         }
 | |
|         updateScrollspy()
 | |
|         if (callback && typeof callback === 'function') { callback() }
 | |
|       }, 1)
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| function checkLayout () {
 | |
|   var navbarHieght = $('.navbar').outerHeight()
 | |
|   $('body').css('padding-top', navbarHieght + 'px')
 | |
| }
 | |
| 
 | |
| function editorHasFocus () {
 | |
|   return $(editor.getInputField()).is(':focus')
 | |
| }
 | |
| 
 | |
| // 768-792px have a gap
 | |
| function checkResponsive () {
 | |
|   visibleXS = $('.visible-xs').is(':visible')
 | |
|   visibleSM = $('.visible-sm').is(':visible')
 | |
|   visibleMD = $('.visible-md').is(':visible')
 | |
|   visibleLG = $('.visible-lg').is(':visible')
 | |
| 
 | |
|   if (visibleXS && window.currentMode === modeType.both) {
 | |
|     if (editorHasFocus()) { changeMode(modeType.edit) } else { changeMode(modeType.view) }
 | |
|   }
 | |
| 
 | |
|   emitUserStatus()
 | |
| }
 | |
| 
 | |
| var lastEditorWidth = 0
 | |
| var previousFocusOnEditor = null
 | |
| 
 | |
| function checkEditorStyle () {
 | |
|   var desireHeight = editorInstance.statusBar ? (ui.area.edit.height() - editorInstance.statusBar.outerHeight()) : ui.area.edit.height()
 | |
|     // set editor height and min height based on scrollbar style and mode
 | |
|   var scrollbarStyle = editor.getOption('scrollbarStyle')
 | |
|   if (scrollbarStyle === 'overlay' || window.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 () {
 | |
|       window.syncscroll = !window.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 (window.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')
 | |
|   }
 | |
| }
 | |
| 
 | |
| var checkEditorScrollbar = _.debounce(function () {
 | |
|   editor.operation(checkEditorScrollbarInner)
 | |
| }, 50)
 | |
| 
 | |
| function checkEditorScrollbarInner () {
 | |
|   // workaround simple scroll bar knob
 | |
|   // will get wrong position when editor height changed
 | |
|   var scrollInfo = editor.getScrollInfo()
 | |
|   editor.scrollTo(null, scrollInfo.top - 1)
 | |
|   editor.scrollTo(null, scrollInfo.top)
 | |
| }
 | |
| 
 | |
| function checkTocStyle () {
 | |
|     // toc right
 | |
|   var paddingRight = parseFloat(ui.area.markdown.css('padding-right'))
 | |
|   var right = ($(window).width() - (ui.area.markdown.offset().left + ui.area.markdown.outerWidth() - paddingRight))
 | |
|   ui.toc.toc.css('right', right + 'px')
 | |
|     // affix toc left
 | |
|   var newbool
 | |
|   var rightMargin = (ui.area.markdown.parent().outerWidth() - ui.area.markdown.outerWidth()) / 2
 | |
|     // for ipad or wider device
 | |
|   if (rightMargin >= 133) {
 | |
|     newbool = true
 | |
|     var affixLeftMargin = (ui.toc.affix.outerWidth() - ui.toc.affix.width()) / 2
 | |
|     var left = ui.area.markdown.offset().left + ui.area.markdown.outerWidth() - affixLeftMargin
 | |
|     ui.toc.affix.css('left', left + 'px')
 | |
|     ui.toc.affix.css('width', rightMargin + 'px')
 | |
|   } else {
 | |
|     newbool = false
 | |
|   }
 | |
|   // toc scrollspy
 | |
|   ui.toc.toc.removeClass('scrollspy-body, scrollspy-view')
 | |
|   ui.toc.affix.removeClass('scrollspy-body, scrollspy-view')
 | |
|   if (window.currentMode === modeType.both) {
 | |
|     ui.toc.toc.addClass('scrollspy-view')
 | |
|     ui.toc.affix.addClass('scrollspy-view')
 | |
|   } else if (window.currentMode !== modeType.both && !newbool) {
 | |
|     ui.toc.toc.addClass('scrollspy-body')
 | |
|     ui.toc.affix.addClass('scrollspy-body')
 | |
|   } else {
 | |
|     ui.toc.toc.addClass('scrollspy-view')
 | |
|     ui.toc.affix.addClass('scrollspy-body')
 | |
|   }
 | |
|   if (newbool !== enoughForAffixToc) {
 | |
|     enoughForAffixToc = newbool
 | |
|     generateScrollspy()
 | |
|   }
 | |
| }
 | |
| 
 | |
| function showStatus (type, num) {
 | |
|   currentStatus = type
 | |
|   var shortStatus = ui.toolbar.shortStatus
 | |
|   var status = ui.toolbar.status
 | |
|   var label = $('<span class="label"></span>')
 | |
|   var fa = $('<i class="fa"></i>')
 | |
|   var msg = ''
 | |
|   var shortMsg = ''
 | |
| 
 | |
|   shortStatus.html('')
 | |
|   status.html('')
 | |
| 
 | |
|   switch (currentStatus) {
 | |
|     case statusType.connected:
 | |
|       label.addClass(statusType.connected.label)
 | |
|       fa.addClass(statusType.connected.fa)
 | |
|       msg = statusType.connected.msg
 | |
|       break
 | |
|     case statusType.online:
 | |
|       label.addClass(statusType.online.label)
 | |
|       fa.addClass(statusType.online.fa)
 | |
|       shortMsg = num
 | |
|       msg = num + ' ' + statusType.online.msg
 | |
|       break
 | |
|     case statusType.offline:
 | |
|       label.addClass(statusType.offline.label)
 | |
|       fa.addClass(statusType.offline.fa)
 | |
|       msg = statusType.offline.msg
 | |
|       break
 | |
|   }
 | |
| 
 | |
|   label.append(fa)
 | |
|   var shortLabel = label.clone()
 | |
| 
 | |
|   shortLabel.append(' ' + shortMsg)
 | |
|   shortStatus.append(shortLabel)
 | |
| 
 | |
|   label.append(' ' + msg)
 | |
|   status.append(label)
 | |
| }
 | |
| 
 | |
| function toggleMode () {
 | |
|   switch (window.currentMode) {
 | |
|     case modeType.edit:
 | |
|       changeMode(modeType.view)
 | |
|       break
 | |
|     case modeType.view:
 | |
|       changeMode(modeType.edit)
 | |
|       break
 | |
|     case modeType.both:
 | |
|       changeMode(modeType.view)
 | |
|       break
 | |
|   }
 | |
| }
 | |
| 
 | |
| var lastMode = null
 | |
| 
 | |
| function changeMode (type) {
 | |
|     // lock navbar to prevent it hide after changeMode
 | |
|   lockNavbar()
 | |
|   saveInfo()
 | |
|   if (type) {
 | |
|     lastMode = window.currentMode
 | |
|     window.currentMode = type
 | |
|   }
 | |
|   var responsiveClass = 'col-lg-6 col-md-6 col-sm-6'
 | |
|   var scrollClass = 'ui-scrollable'
 | |
|   ui.area.codemirror.removeClass(scrollClass)
 | |
|   ui.area.edit.removeClass(responsiveClass)
 | |
|   ui.area.view.removeClass(scrollClass)
 | |
|   ui.area.view.removeClass(responsiveClass)
 | |
|   switch (window.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 + '?' + window.currentMode.name)
 | |
|   if (window.currentMode === modeType.view) {
 | |
|     editor.getInputField().blur()
 | |
|   }
 | |
|   if (window.currentMode === modeType.edit || window.currentMode === modeType.both) {
 | |
|     ui.toolbar.uploadImage.fadeIn()
 | |
|     // add and update status bar
 | |
|     if (!editorInstance.statusBar) {
 | |
|       editorInstance.addStatusBar()
 | |
|       editorInstance.updateStatusBar()
 | |
|     }
 | |
|     // work around foldGutter might not init properly
 | |
|     editor.setOption('foldGutter', false)
 | |
|     editor.setOption('foldGutter', true)
 | |
|   } else {
 | |
|     ui.toolbar.uploadImage.fadeOut()
 | |
|   }
 | |
|   if (window.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 (window.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 && window.currentMode === modeType.both) {
 | |
|     window.preventSyncScrollToView = 2
 | |
|     syncScrollToEdit(null, true)
 | |
|   }
 | |
| 
 | |
|   if (lastMode === modeType.edit && window.currentMode === modeType.both) {
 | |
|     window.preventSyncScrollToEdit = 2
 | |
|     syncScrollToView(null, true)
 | |
|   }
 | |
| 
 | |
|   if (lastMode === modeType.both && window.currentMode !== modeType.both) {
 | |
|     window.preventSyncScrollToView = false
 | |
|     window.preventSyncScrollToEdit = false
 | |
|   }
 | |
| 
 | |
|   if (lastMode !== modeType.edit && window.currentMode === modeType.edit) {
 | |
|     editor.refresh()
 | |
|   }
 | |
| 
 | |
|   $(document.body).scrollspy('refresh')
 | |
|   ui.area.view.scrollspy('refresh')
 | |
| 
 | |
|   ui.toolbar.both.removeClass('active')
 | |
|   ui.toolbar.edit.removeClass('active')
 | |
|   ui.toolbar.view.removeClass('active')
 | |
|   var modeIcon = ui.toolbar.mode.find('i')
 | |
|   modeIcon.removeClass('fa-pencil').removeClass('fa-eye')
 | |
|   if (ui.area.edit.is(':visible') && ui.area.view.is(':visible')) { // both
 | |
|     ui.toolbar.both.addClass('active')
 | |
|     modeIcon.addClass('fa-eye')
 | |
|   } else if (ui.area.edit.is(':visible')) { // edit
 | |
|     ui.toolbar.edit.addClass('active')
 | |
|     modeIcon.addClass('fa-eye')
 | |
|   } else if (ui.area.view.is(':visible')) { // view
 | |
|     ui.toolbar.view.addClass('active')
 | |
|     modeIcon.addClass('fa-pencil')
 | |
|   }
 | |
|   unlockNavbar()
 | |
| }
 | |
| 
 | |
| function lockNavbar () {
 | |
|   $('.navbar').addClass('locked')
 | |
| }
 | |
| 
 | |
| var unlockNavbar = _.debounce(function () {
 | |
|   $('.navbar').removeClass('locked')
 | |
| }, 200)
 | |
| 
 | |
| function showMessageModal (title, header, href, text, success) {
 | |
|   var modal = $('.message-modal')
 | |
|   modal.find('.modal-title').html(title)
 | |
|   modal.find('.modal-body h5').html(header)
 | |
|   if (href) { modal.find('.modal-body a').attr('href', href).text(text) } else { modal.find('.modal-body a').removeAttr('href').text(text) }
 | |
|   modal.find('.modal-footer button').removeClass('btn-default btn-success btn-danger')
 | |
|   if (success) { modal.find('.modal-footer button').addClass('btn-success') } else { modal.find('.modal-footer button').addClass('btn-danger') }
 | |
|   modal.modal('show')
 | |
| }
 | |
| 
 | |
| // check if dropbox app key is set and load scripts
 | |
| if (DROPBOX_APP_KEY) {
 | |
|   $('<script>')
 | |
|         .attr('type', 'text/javascript')
 | |
|         .attr('src', 'https://www.dropbox.com/static/api/2/dropins.js')
 | |
|         .attr('id', 'dropboxjs')
 | |
|         .attr('data-app-key', DROPBOX_APP_KEY)
 | |
|         .prop('async', true)
 | |
|         .prop('defer', true)
 | |
|         .appendTo('body')
 | |
| } else {
 | |
|   ui.toolbar.import.dropbox.hide()
 | |
|   ui.toolbar.export.dropbox.hide()
 | |
| }
 | |
| 
 | |
| // check if google api key and client id are set and load scripts
 | |
| if (GOOGLE_API_KEY && GOOGLE_CLIENT_ID) {
 | |
|   $('<script>')
 | |
|         .attr('type', 'text/javascript')
 | |
|         .attr('src', 'https://www.google.com/jsapi?callback=onGoogleAPILoaded')
 | |
|         .prop('async', true)
 | |
|         .prop('defer', true)
 | |
|         .appendTo('body')
 | |
| } else {
 | |
|   ui.toolbar.import.googleDrive.hide()
 | |
|   ui.toolbar.export.googleDrive.hide()
 | |
| }
 | |
| 
 | |
| function onGoogleAPILoaded () {
 | |
|   $('<script>')
 | |
|         .attr('type', 'text/javascript')
 | |
|         .attr('src', 'https://apis.google.com/js/client:plusone.js?onload=onGoogleClientLoaded')
 | |
|         .prop('async', true)
 | |
|         .prop('defer', true)
 | |
|         .appendTo('body')
 | |
| }
 | |
| window.onGoogleAPILoaded = onGoogleAPILoaded
 | |
| 
 | |
| // button actions
 | |
| // share
 | |
| ui.toolbar.publish.attr('href', noteurl + '/publish')
 | |
| // extra
 | |
| // slide
 | |
| ui.toolbar.extra.slide.attr('href', noteurl + '/slide')
 | |
| // download
 | |
| // markdown
 | |
| ui.toolbar.download.markdown.click(function (e) {
 | |
|   e.preventDefault()
 | |
|   e.stopPropagation()
 | |
|   var filename = renderFilename(ui.area.markdown) + '.md'
 | |
|   var markdown = editor.getValue()
 | |
|   var blob = new Blob([markdown], {
 | |
|     type: 'text/markdown;charset=utf-8'
 | |
|   })
 | |
|   saveAs(blob, filename, 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)
 | |
| })
 | |
| // pdf
 | |
| ui.toolbar.download.pdf.attr('download', '').attr('href', noteurl + '/pdf')
 | |
| // export to dropbox
 | |
| ui.toolbar.export.dropbox.click(function () {
 | |
|   var filename = renderFilename(ui.area.markdown) + '.md'
 | |
|   var options = {
 | |
|     files: [
 | |
|       {
 | |
|         'url': noteurl + '/download',
 | |
|         'filename': filename
 | |
|       }
 | |
|     ],
 | |
|     error: function (errorMessage) {
 | |
|       console.error(errorMessage)
 | |
|     }
 | |
|   }
 | |
|   Dropbox.save(options)
 | |
| })
 | |
| function uploadToGoogleDrive (accessToken) {
 | |
|   ui.spinner.show()
 | |
|   var filename = renderFilename(ui.area.markdown) + '.md'
 | |
|   var markdown = editor.getValue()
 | |
|   var blob = new Blob([markdown], {
 | |
|     type: 'text/markdown;charset=utf-8'
 | |
|   })
 | |
|   blob.name = filename
 | |
|   var uploader = new MediaUploader({
 | |
|     file: blob,
 | |
|     token: accessToken,
 | |
|     onComplete: function (data) {
 | |
|       data = JSON.parse(data)
 | |
|       showMessageModal('<i class="fa fa-cloud-upload"></i> Export to Google Drive', 'Export Complete!', data.alternateLink, 'Click here to view your file', true)
 | |
|       ui.spinner.hide()
 | |
|     },
 | |
|     onError: function (data) {
 | |
|       showMessageModal('<i class="fa fa-cloud-upload"></i> Export to Google Drive', 'Export Error :(', '', data, false)
 | |
|       ui.spinner.hide()
 | |
|     }
 | |
|   })
 | |
|   uploader.upload()
 | |
| }
 | |
| function googleApiAuth (immediate, callback) {
 | |
|   gapi.auth.authorize(
 | |
|     {
 | |
|       'client_id': GOOGLE_CLIENT_ID,
 | |
|       'scope': 'https://www.googleapis.com/auth/drive.file',
 | |
|       'immediate': immediate
 | |
|     }, callback || function () { })
 | |
| }
 | |
| function onGoogleClientLoaded () {
 | |
|   googleApiAuth(true)
 | |
|   buildImportFromGoogleDrive()
 | |
| }
 | |
| window.onGoogleClientLoaded = onGoogleClientLoaded
 | |
| // export to google drive
 | |
| ui.toolbar.export.googleDrive.click(function (e) {
 | |
|   var token = gapi.auth.getToken()
 | |
|   if (token) {
 | |
|     uploadToGoogleDrive(token.access_token)
 | |
|   } else {
 | |
|     googleApiAuth(false, function (result) {
 | |
|       uploadToGoogleDrive(result.access_token)
 | |
|     })
 | |
|   }
 | |
| })
 | |
| // export to gist
 | |
| ui.toolbar.export.gist.attr('href', noteurl + '/gist')
 | |
| // export to snippet
 | |
| ui.toolbar.export.snippet.click(function () {
 | |
|   ui.spinner.show()
 | |
|   $.get(serverurl + '/auth/gitlab/callback/' + noteid + '/projects')
 | |
|         .done(function (data) {
 | |
|           $('#snippetExportModalAccessToken').val(data.accesstoken)
 | |
|           $('#snippetExportModalBaseURL').val(data.baseURL)
 | |
|           $('#snippetExportModalLoading').hide()
 | |
|           $('#snippetExportModal').modal('toggle')
 | |
|           $('#snippetExportModalProjects').find('option').remove().end().append('<option value="init" selected="selected" disabled="disabled">Select From Available Projects</option>')
 | |
|           if (data.projects) {
 | |
|             data.projects.sort(function (a, b) {
 | |
|               return (a.path_with_namespace < b.path_with_namespace) ? -1 : ((a.path_with_namespace > b.path_with_namespace) ? 1 : 0)
 | |
|             })
 | |
|             data.projects.forEach(function (project) {
 | |
|               if (!project.snippets_enabled ||
 | |
|                         (project.permissions.project_access === null && project.permissions.group_access === null) ||
 | |
|                         (project.permissions.project_access !== null && project.permissions.project_access.access_level < 20)) {
 | |
|                 return
 | |
|               }
 | |
|               $('<option>').val(project.id).text(project.path_with_namespace).appendTo('#snippetExportModalProjects')
 | |
|             })
 | |
|             $('#snippetExportModalProjects').prop('disabled', false)
 | |
|           }
 | |
|           $('#snippetExportModalLoading').hide()
 | |
|         })
 | |
|         .fail(function (data) {
 | |
|           showMessageModal('<i class="fa fa-gitlab"></i> Import from Snippet', 'Unable to fetch gitlab parameters :(', '', '', false)
 | |
|         })
 | |
|         .always(function () {
 | |
|           ui.spinner.hide()
 | |
|         })
 | |
| })
 | |
| // import from dropbox
 | |
| ui.toolbar.import.dropbox.click(function () {
 | |
|   var options = {
 | |
|     success: function (files) {
 | |
|       ui.spinner.show()
 | |
|       var url = files[0].link
 | |
|       importFromUrl(url)
 | |
|     },
 | |
|     linkType: 'direct',
 | |
|     multiselect: false,
 | |
|     extensions: ['.md', '.html']
 | |
|   }
 | |
|   Dropbox.choose(options)
 | |
| })
 | |
| // import from google drive
 | |
| function buildImportFromGoogleDrive () {
 | |
|   /* eslint-disable no-unused-vars */
 | |
|   let picker = new FilePicker({
 | |
|     apiKey: GOOGLE_API_KEY,
 | |
|     clientId: GOOGLE_CLIENT_ID,
 | |
|     buttonEl: ui.toolbar.import.googleDrive,
 | |
|     onSelect: function (file) {
 | |
|       if (file.downloadUrl) {
 | |
|         ui.spinner.show()
 | |
|         var accessToken = gapi.auth.getToken().access_token
 | |
|         $.ajax({
 | |
|           type: 'GET',
 | |
|           beforeSend: function (request) {
 | |
|             request.setRequestHeader('Authorization', 'Bearer ' + accessToken)
 | |
|           },
 | |
|           url: file.downloadUrl,
 | |
|           success: function (data) {
 | |
|             if (file.fileExtension === 'html') { parseToEditor(data) } else { replaceAll(data) }
 | |
|           },
 | |
|           error: function (data) {
 | |
|             showMessageModal('<i class="fa fa-cloud-download"></i> Import from Google Drive', 'Import failed :(', '', data, false)
 | |
|           },
 | |
|           complete: function () {
 | |
|             ui.spinner.hide()
 | |
|           }
 | |
|         })
 | |
|       }
 | |
|     }
 | |
|   })
 | |
|   /* eslint-enable no-unused-vars */
 | |
| }
 | |
| // import from gist
 | |
| ui.toolbar.import.gist.click(function () {
 | |
|     // na
 | |
| })
 | |
| // import from snippet
 | |
| ui.toolbar.import.snippet.click(function () {
 | |
|   ui.spinner.show()
 | |
|   $.get(serverurl + '/auth/gitlab/callback/' + noteid + '/projects')
 | |
|         .done(function (data) {
 | |
|           $('#snippetImportModalAccessToken').val(data.accesstoken)
 | |
|           $('#snippetImportModalBaseURL').val(data.baseURL)
 | |
|           $('#snippetImportModalContent').prop('disabled', false)
 | |
|           $('#snippetImportModalConfirm').prop('disabled', false)
 | |
|           $('#snippetImportModalLoading').hide()
 | |
|           $('#snippetImportModal').modal('toggle')
 | |
|           $('#snippetImportModalProjects').find('option').remove().end().append('<option value="init" selected="selected" disabled="disabled">Select From Available Projects</option>')
 | |
|           if (data.projects) {
 | |
|             data.projects.sort(function (a, b) {
 | |
|               return (a.path_with_namespace < b.path_with_namespace) ? -1 : ((a.path_with_namespace > b.path_with_namespace) ? 1 : 0)
 | |
|             })
 | |
|             data.projects.forEach(function (project) {
 | |
|               if (!project.snippets_enabled ||
 | |
|                         (project.permissions.project_access === null && project.permissions.group_access === null) ||
 | |
|                         (project.permissions.project_access !== null && project.permissions.project_access.access_level < 20)) {
 | |
|                 return
 | |
|               }
 | |
|               $('<option>').val(project.id).text(project.path_with_namespace).appendTo('#snippetImportModalProjects')
 | |
|             })
 | |
|             $('#snippetImportModalProjects').prop('disabled', false)
 | |
|           }
 | |
|           $('#snippetImportModalLoading').hide()
 | |
|         })
 | |
|         .fail(function (data) {
 | |
|           showMessageModal('<i class="fa fa-gitlab"></i> Import from Snippet', 'Unable to fetch gitlab parameters :(', '', '', false)
 | |
|         })
 | |
|         .always(function () {
 | |
|           ui.spinner.hide()
 | |
|         })
 | |
| })
 | |
| // import from clipboard
 | |
| ui.toolbar.import.clipboard.click(function () {
 | |
|     // na
 | |
| })
 | |
| // upload image
 | |
| ui.toolbar.uploadImage.bind('change', function (e) {
 | |
|   var files = e.target.files || e.dataTransfer.files
 | |
|   e.dataTransfer = {}
 | |
|   e.dataTransfer.files = files
 | |
|   inlineAttach.onDrop(e)
 | |
| })
 | |
| // toc
 | |
| ui.toc.dropdown.click(function (e) {
 | |
|   e.stopPropagation()
 | |
| })
 | |
| // prevent empty link change hash
 | |
| $('a[href="#"]').click(function (e) {
 | |
|   e.preventDefault()
 | |
| })
 | |
| 
 | |
| // modal actions
 | |
| var revisions = []
 | |
| var revisionViewer = null
 | |
| var revisionInsert = []
 | |
| var revisionDelete = []
 | |
| var revisionInsertAnnotation = null
 | |
| var revisionDeleteAnnotation = null
 | |
| var revisionList = ui.modal.revision.find('.ui-revision-list')
 | |
| var revision = null
 | |
| var revisionTime = null
 | |
| ui.modal.revision.on('show.bs.modal', function (e) {
 | |
|   $.get(noteurl + '/revision')
 | |
|         .done(function (data) {
 | |
|           parseRevisions(data.revision)
 | |
|           initRevisionViewer()
 | |
|         })
 | |
|         .fail(function (err) {
 | |
|           if (debug) {
 | |
|             console.log(err)
 | |
|           }
 | |
|         })
 | |
|         .always(function () {
 | |
|             // na
 | |
|         })
 | |
| })
 | |
| function checkRevisionViewer () {
 | |
|   if (revisionViewer) {
 | |
|     var container = $(revisionViewer.display.wrapper).parent()
 | |
|     $(revisionViewer.display.scroller).css('height', container.height() + 'px')
 | |
|     revisionViewer.refresh()
 | |
|   }
 | |
| }
 | |
| ui.modal.revision.on('shown.bs.modal', checkRevisionViewer)
 | |
| $(window).resize(checkRevisionViewer)
 | |
| function parseRevisions (_revisions) {
 | |
|   if (_revisions.length !== revisions) {
 | |
|     revisions = _revisions
 | |
|     var lastRevision = null
 | |
|     if (revisionList.children().length > 0) {
 | |
|       lastRevision = revisionList.find('.active').attr('data-revision-time')
 | |
|     }
 | |
|     revisionList.html('')
 | |
|     for (var i = 0; i < revisions.length; i++) {
 | |
|       var revision = revisions[i]
 | |
|       var item = $('<a href="#" class="list-group-item"></a>')
 | |
|       item.attr('data-revision-time', revision.time)
 | |
|       if (lastRevision === revision.time) item.addClass('active')
 | |
|       var itemHeading = $('<h5 class="list-group-item-heading"></h5>')
 | |
|       itemHeading.html('<i class="fa fa-clock-o"></i> ' + moment(revision.time).format('llll'))
 | |
|       var itemText = $('<p class="list-group-item-text"></p>')
 | |
|       itemText.html('<i class="fa fa-file-text"></i> Length: ' + revision.length)
 | |
|       item.append(itemHeading).append(itemText)
 | |
|       item.click(function (e) {
 | |
|         var time = $(this).attr('data-revision-time')
 | |
|         selectRevision(time)
 | |
|       })
 | |
|       revisionList.append(item)
 | |
|     }
 | |
|     if (!lastRevision) {
 | |
|       selectRevision(revisions[0].time)
 | |
|     }
 | |
|   }
 | |
| }
 | |
| function selectRevision (time) {
 | |
|   if (time === revisionTime) return
 | |
|   $.get(noteurl + '/revision/' + time)
 | |
|         .done(function (data) {
 | |
|           revision = data
 | |
|           revisionTime = time
 | |
|           var lastScrollInfo = revisionViewer.getScrollInfo()
 | |
|           revisionList.children().removeClass('active')
 | |
|           revisionList.find('[data-revision-time="' + time + '"]').addClass('active')
 | |
|           var content = revision.content
 | |
|           revisionViewer.setValue(content)
 | |
|           revisionViewer.scrollTo(null, lastScrollInfo.top)
 | |
|           revisionInsert = []
 | |
|           revisionDelete = []
 | |
|             // mark the text which have been insert or delete
 | |
|           if (revision.patch.length > 0) {
 | |
|             var bias = 0
 | |
|             for (var j = 0; j < revision.patch.length; j++) {
 | |
|               var patch = revision.patch[j]
 | |
|               var currIndex = patch.start1 + bias
 | |
|               for (var i = 0; i < patch.diffs.length; i++) {
 | |
|                 var diff = patch.diffs[i]
 | |
|                 // ignore if diff only contains line breaks
 | |
|                 if ((diff[1].match(/\n/g) || []).length === diff[1].length) continue
 | |
|                 var prePos
 | |
|                 var 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) {
 | |
|             console.log(err)
 | |
|           }
 | |
|         })
 | |
|         .always(function () {
 | |
|             // na
 | |
|         })
 | |
| }
 | |
| function initRevisionViewer () {
 | |
|   if (revisionViewer) return
 | |
|   var 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
 | |
|   var filename = renderFilename(ui.area.markdown) + '_' + revisionTime + '.md'
 | |
|   var 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 () {
 | |
|   var accesstoken = $('#snippetImportModalAccessToken').val()
 | |
|   var baseURL = $('#snippetImportModalBaseURL').val()
 | |
|   var project = $('#snippetImportModalProjects').val()
 | |
| 
 | |
|   $('#snippetImportModalLoading').show()
 | |
|   $('#snippetImportModalContent').val('/projects/' + project)
 | |
|   $.get(baseURL + '/api/v3/projects/' + project + '/snippets?access_token=' + accesstoken)
 | |
|         .done(function (data) {
 | |
|           $('#snippetImportModalSnippets').find('option').remove().end().append('<option value="init" selected="selected" disabled="disabled">Select From Available Snippets</option>')
 | |
|           data.forEach(function (snippet) {
 | |
|             $('<option>').val(snippet.id).text(snippet.title).appendTo($('#snippetImportModalSnippets'))
 | |
|           })
 | |
|           $('#snippetImportModalLoading').hide()
 | |
|           $('#snippetImportModalSnippets').prop('disabled', false)
 | |
|         })
 | |
|         .fail(function (err) {
 | |
|           if (debug) {
 | |
|             console.log(err)
 | |
|           }
 | |
|         })
 | |
|         .always(function () {
 | |
|             // na
 | |
|         })
 | |
| })
 | |
| // snippet snippets
 | |
| ui.modal.snippetImportSnippets.change(function () {
 | |
|   var snippet = $('#snippetImportModalSnippets').val()
 | |
|   $('#snippetImportModalContent').val($('#snippetImportModalContent').val() + '/snippets/' + snippet)
 | |
| })
 | |
| 
 | |
| function scrollToTop () {
 | |
|   if (window.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 (window.currentMode === modeType.both) {
 | |
|     var scrollInfo = editor.getScrollInfo()
 | |
|     var scrollHeight = scrollInfo.height
 | |
|     if (scrollInfo.top !== scrollHeight) { editor.scrollTo(0, scrollHeight * 2) } else {
 | |
|       ui.area.view.animate({
 | |
|         scrollTop: ui.area.view[0].scrollHeight
 | |
|       }, 100, 'linear')
 | |
|     }
 | |
|   } else {
 | |
|     $('body, html').stop(true, true).animate({
 | |
|       scrollTop: $(document.body)[0].scrollHeight
 | |
|     }, 100, 'linear')
 | |
|   }
 | |
| }
 | |
| 
 | |
| window.scrollToTop = scrollToTop
 | |
| window.scrollToBottom = scrollToBottom
 | |
| 
 | |
| var enoughForAffixToc = true
 | |
| 
 | |
| // scrollspy
 | |
| function generateScrollspy () {
 | |
|   $(document.body).scrollspy({
 | |
|     target: '.scrollspy-body'
 | |
|   })
 | |
|   ui.area.view.scrollspy({
 | |
|     target: '.scrollspy-view'
 | |
|   })
 | |
|   $(document.body).scrollspy('refresh')
 | |
|   ui.area.view.scrollspy('refresh')
 | |
|   if (enoughForAffixToc) {
 | |
|     ui.toc.toc.hide()
 | |
|     ui.toc.affix.show()
 | |
|   } else {
 | |
|     ui.toc.affix.hide()
 | |
|     ui.toc.toc.show()
 | |
|   }
 | |
|     // $(document.body).scroll();
 | |
|     // ui.area.view.scroll();
 | |
| }
 | |
| 
 | |
| function updateScrollspy () {
 | |
|   var headers = ui.area.markdown.find('h1, h2, h3').toArray()
 | |
|   var headerMap = []
 | |
|   for (var i = 0; i < headers.length; i++) {
 | |
|     headerMap.push($(headers[i]).offset().top - parseInt($(headers[i]).css('margin-top')))
 | |
|   }
 | |
|   applyScrollspyActive($(window).scrollTop(), headerMap, headers,
 | |
|         $('.scrollspy-body'), 0)
 | |
|   var offset = ui.area.view.scrollTop() - ui.area.view.offset().top
 | |
|   applyScrollspyActive(ui.area.view.scrollTop(), headerMap, headers,
 | |
|         $('.scrollspy-view'), offset - 10)
 | |
| }
 | |
| 
 | |
| function applyScrollspyActive (top, headerMap, headers, target, offset) {
 | |
|   var index = 0
 | |
|   for (var i = headerMap.length - 1; i >= 0; i--) {
 | |
|     if (top >= (headerMap[i] + offset) && headerMap[i + 1] && top < (headerMap[i + 1] + offset)) {
 | |
|       index = i
 | |
|       break
 | |
|     }
 | |
|   }
 | |
|   var header = $(headers[index])
 | |
|   var active = target.find('a[href="#' + header.attr('id') + '"]')
 | |
|   active.closest('li').addClass('active').parent().closest('li').addClass('active').parent().closest('li').addClass('active')
 | |
| }
 | |
| 
 | |
| // clipboard modal
 | |
| // fix for wrong autofocus
 | |
| $('#clipboardModal').on('shown.bs.modal', function () {
 | |
|   $('#clipboardModal').blur()
 | |
| })
 | |
| $('#clipboardModalClear').click(function () {
 | |
|   $('#clipboardModalContent').html('')
 | |
| })
 | |
| $('#clipboardModalConfirm').click(function () {
 | |
|   var data = $('#clipboardModalContent').html()
 | |
|   if (data) {
 | |
|     parseToEditor(data)
 | |
|     $('#clipboardModal').modal('hide')
 | |
|     $('#clipboardModalContent').html('')
 | |
|   }
 | |
| })
 | |
| 
 | |
| // refresh modal
 | |
| $('#refreshModalRefresh').click(function () {
 | |
|   location.reload(true)
 | |
| })
 | |
| 
 | |
| // gist import modal
 | |
| $('#gistImportModalClear').click(function () {
 | |
|   $('#gistImportModalContent').val('')
 | |
| })
 | |
| $('#gistImportModalConfirm').click(function () {
 | |
|   var gisturl = $('#gistImportModalContent').val()
 | |
|   if (!gisturl) return
 | |
|   $('#gistImportModal').modal('hide')
 | |
|   $('#gistImportModalContent').val('')
 | |
|   if (!isValidURL(gisturl)) {
 | |
|     showMessageModal('<i class="fa fa-github"></i> Import from Gist', 'Not a valid URL :(', '', '', false)
 | |
|   } else {
 | |
|     var hostname = window.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/' + window.url('-1', gisturl))
 | |
|                 .done(function (data) {
 | |
|                   if (data.files) {
 | |
|                     var contents = ''
 | |
|                     Object.keys(data.files).forEach(function (key) {
 | |
|                       contents += key
 | |
|                       contents += '\n---\n'
 | |
|                       contents += data.files[key].content
 | |
|                       contents += '\n\n'
 | |
|                     })
 | |
|                     replaceAll(contents)
 | |
|                   } else {
 | |
|                     showMessageModal('<i class="fa fa-github"></i> Import from Gist', 'Unable to fetch gist files :(', '', '', false)
 | |
|                   }
 | |
|                 })
 | |
|                 .fail(function (data) {
 | |
|                   showMessageModal('<i class="fa fa-github"></i> Import from Gist', 'Not a valid Gist URL :(', '', JSON.stringify(data), false)
 | |
|                 })
 | |
|                 .always(function () {
 | |
|                   ui.spinner.hide()
 | |
|                 })
 | |
|     }
 | |
|   }
 | |
| })
 | |
| 
 | |
| // snippet import modal
 | |
| $('#snippetImportModalClear').click(function () {
 | |
|   $('#snippetImportModalContent').val('')
 | |
|   $('#snippetImportModalProjects').val('init')
 | |
|   $('#snippetImportModalSnippets').val('init')
 | |
|   $('#snippetImportModalSnippets').prop('disabled', true)
 | |
| })
 | |
| $('#snippetImportModalConfirm').click(function () {
 | |
|   var snippeturl = $('#snippetImportModalContent').val()
 | |
|   if (!snippeturl) return
 | |
|   $('#snippetImportModal').modal('hide')
 | |
|   $('#snippetImportModalContent').val('')
 | |
|   if (!/^.+\/snippets\/.+$/.test(snippeturl)) {
 | |
|     showMessageModal('<i class="fa fa-github"></i> Import from Snippet', 'Not a valid Snippet URL :(', '', '', false)
 | |
|   } else {
 | |
|     ui.spinner.show()
 | |
|     var accessToken = '?access_token=' + $('#snippetImportModalAccessToken').val()
 | |
|     var fullURL = $('#snippetImportModalBaseURL').val() + '/api/v3' + snippeturl
 | |
|     $.get(fullURL + accessToken)
 | |
|             .done(function (data) {
 | |
|               var content = '# ' + (data.title || 'Snippet Import')
 | |
|               var fileInfo = data.file_name.split('.')
 | |
|               fileInfo[1] = (fileInfo[1]) ? fileInfo[1] : 'md'
 | |
|               $.get(fullURL + '/raw' + accessToken)
 | |
|                     .done(function (raw) {
 | |
|                       if (raw) {
 | |
|                         content += '\n\n'
 | |
|                         if (fileInfo[1] !== 'md') {
 | |
|                           content += '```' + fileTypes[fileInfo[1]] + '\n'
 | |
|                         }
 | |
|                         content += raw
 | |
|                         if (fileInfo[1] !== 'md') {
 | |
|                           content += '\n```'
 | |
|                         }
 | |
|                         replaceAll(content)
 | |
|                       }
 | |
|                     })
 | |
|                     .fail(function (data) {
 | |
|                       showMessageModal('<i class="fa fa-gitlab"></i> Import from Snippet', 'Not a valid Snippet URL :(', '', JSON.stringify(data), false)
 | |
|                     })
 | |
|                     .always(function () {
 | |
|                       ui.spinner.hide()
 | |
|                     })
 | |
|             })
 | |
|             .fail(function (data) {
 | |
|               showMessageModal('<i class="fa fa-gitlab"></i> Import from Snippet', 'Not a valid Snippet URL :(', '', JSON.stringify(data), false)
 | |
|             })
 | |
|   }
 | |
| })
 | |
| 
 | |
| // snippet export modal
 | |
| $('#snippetExportModalConfirm').click(function () {
 | |
|   var accesstoken = $('#snippetExportModalAccessToken').val()
 | |
|   var baseURL = $('#snippetExportModalBaseURL').val()
 | |
|   var data = {
 | |
|     title: $('#snippetExportModalTitle').val(),
 | |
|     file_name: $('#snippetExportModalFileName').val(),
 | |
|     code: editor.getValue(),
 | |
|     visibility_level: $('#snippetExportModalVisibility').val()
 | |
|   }
 | |
|   if (!data.title || !data.file_name || !data.code || !data.visibility_level || !$('#snippetExportModalProjects').val()) return
 | |
|   $('#snippetExportModalLoading').show()
 | |
|   var fullURL = baseURL + '/api/v3/projects/' + $('#snippetExportModalProjects').val() + '/snippets?access_token=' + accesstoken
 | |
|   $.post(fullURL
 | |
|         , data
 | |
|         , function (ret) {
 | |
|           $('#snippetExportModalLoading').hide()
 | |
|           $('#snippetExportModal').modal('hide')
 | |
|           var redirect = baseURL + '/' + $("#snippetExportModalProjects option[value='" + $('#snippetExportModalProjects').val() + "']").text() + '/snippets/' + ret.id
 | |
|           showMessageModal('<i class="fa fa-gitlab"></i> Export to Snippet', 'Export Successful!', redirect, 'View Snippet Here', true)
 | |
|         }
 | |
|         , 'json'
 | |
|     )
 | |
| })
 | |
| 
 | |
| function parseToEditor (data) {
 | |
|   var parsed = toMarkdown(data)
 | |
|   if (parsed) { replaceAll(parsed) }
 | |
| }
 | |
| 
 | |
| function replaceAll (data) {
 | |
|   editor.replaceRange(data, {
 | |
|     line: 0,
 | |
|     ch: 0
 | |
|   }, {
 | |
|     line: editor.lastLine(),
 | |
|     ch: editor.lastLine().length
 | |
|   }, '+input')
 | |
| }
 | |
| 
 | |
| function importFromUrl (url) {
 | |
|     // console.log(url);
 | |
|   if (!url) return
 | |
|   if (!isValidURL(url)) {
 | |
|     showMessageModal('<i class="fa fa-cloud-download"></i> Import from URL', 'Not a valid URL :(', '', '', false)
 | |
|     return
 | |
|   }
 | |
|   $.ajax({
 | |
|     method: 'GET',
 | |
|     url: url,
 | |
|     success: function (data) {
 | |
|       var extension = url.split('.').pop()
 | |
|       if (extension === 'html') { parseToEditor(data) } else { replaceAll(data) }
 | |
|     },
 | |
|     error: function (data) {
 | |
|       showMessageModal('<i class="fa fa-cloud-download"></i> Import from URL', 'Import failed :(', '', JSON.stringify(data), false)
 | |
|     },
 | |
|     complete: function () {
 | |
|       ui.spinner.hide()
 | |
|     }
 | |
|   })
 | |
| }
 | |
| 
 | |
| // mode
 | |
| ui.toolbar.mode.click(function () {
 | |
|   toggleMode()
 | |
| })
 | |
| // edit
 | |
| ui.toolbar.edit.click(function () {
 | |
|   changeMode(modeType.edit)
 | |
| })
 | |
| // view
 | |
| ui.toolbar.view.click(function () {
 | |
|   changeMode(modeType.view)
 | |
| })
 | |
| // both
 | |
| ui.toolbar.both.click(function () {
 | |
|   changeMode(modeType.both)
 | |
| })
 | |
| // permission
 | |
| // freely
 | |
| ui.infobar.permission.freely.click(function () {
 | |
|   emitPermission('freely')
 | |
| })
 | |
| // editable
 | |
| ui.infobar.permission.editable.click(function () {
 | |
|   emitPermission('editable')
 | |
| })
 | |
| // locked
 | |
| ui.infobar.permission.locked.click(function () {
 | |
|   emitPermission('locked')
 | |
| })
 | |
| // private
 | |
| ui.infobar.permission.private.click(function () {
 | |
|   emitPermission('private')
 | |
| })
 | |
| // 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 emitPermission (_permission) {
 | |
|   if (_permission !== permission) {
 | |
|     socket.emit('permission', _permission)
 | |
|   }
 | |
| }
 | |
| 
 | |
| function updatePermission (newPermission) {
 | |
|   if (permission !== newPermission) {
 | |
|     permission = newPermission
 | |
|     if (window.loaded) refreshView()
 | |
|   }
 | |
|   var label = null
 | |
|   var title = null
 | |
|   switch (permission) {
 | |
|     case 'freely':
 | |
|       label = '<i class="fa fa-leaf"></i> Freely'
 | |
|       title = 'Anyone can edit'
 | |
|       break
 | |
|     case 'editable':
 | |
|       label = '<i class="fa fa-shield"></i> Editable'
 | |
|       title = 'Signed people can edit'
 | |
|       break
 | |
|     case '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 () {
 | |
|   var 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
 | |
| var io = require('socket.io-client')
 | |
| var socket = io.connect({
 | |
|   path: urlpath ? '/' + urlpath + '/socket.io/' : '',
 | |
|   timeout: 5000, // 5 secs to timeout,
 | |
|   reconnectionAttempts: 20 // retry 20 times on connect failed
 | |
| })
 | |
| // overwrite original event for checking login state
 | |
| var on = socket.on
 | |
| socket.on = function () {
 | |
|   if (!checkLoginStateChanged() && !needRefresh) { return on.apply(socket, arguments) }
 | |
| }
 | |
| var emit = socket.emit
 | |
| socket.emit = function () {
 | |
|   if (!checkLoginStateChanged() && !needRefresh) { emit.apply(socket, arguments) }
 | |
| }
 | |
| socket.on('info', function (data) {
 | |
|   console.error(data)
 | |
|   switch (data.code) {
 | |
|     case 403:
 | |
|       location.href = 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) {
 | |
|       var newnotehistory = removeHistory(noteid, notehistory)
 | |
|       saveHistory(newnotehistory)
 | |
|       location.href = serverurl
 | |
|     })
 | |
|   }
 | |
| })
 | |
| var 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')
 | |
|     }
 | |
|   }
 | |
| })
 | |
| var authors = []
 | |
| var authorship = []
 | |
| var authorMarks = {} // temp variable
 | |
| var addTextMarkers = [] // temp variable
 | |
| function updateInfo (data) {
 | |
|     // console.log(data);
 | |
|   if (data.hasOwnProperty('createtime') && window.createtime !== data.createtime) {
 | |
|     window.createtime = data.createtime
 | |
|     updateLastChange()
 | |
|   }
 | |
|   if (data.hasOwnProperty('updatetime') && window.lastchangetime !== data.updatetime) {
 | |
|     window.lastchangetime = data.updatetime
 | |
|     updateLastChange()
 | |
|   }
 | |
|   if (data.hasOwnProperty('owner') && window.owner !== data.owner) {
 | |
|     window.owner = data.owner
 | |
|     window.ownerprofile = data.ownerprofile
 | |
|     updateOwner()
 | |
|   }
 | |
|   if (data.hasOwnProperty('lastchangeuser') && window.lastchangeuser !== data.lastchangeuser) {
 | |
|     window.lastchangeuser = data.lastchangeuser
 | |
|     window.lastchangeuserprofile = data.lastchangeuserprofile
 | |
|     updateLastChangeUser()
 | |
|     updateOwner()
 | |
|   }
 | |
|   if (data.hasOwnProperty('authors') && authors !== data.authors) {
 | |
|     authors = data.authors
 | |
|   }
 | |
|   if (data.hasOwnProperty('authorship') && authorship !== data.authorship) {
 | |
|     authorship = data.authorship
 | |
|     updateAuthorship()
 | |
|   }
 | |
| }
 | |
| var 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
 | |
| }
 | |
| var addStyleRule = (function () {
 | |
|   var added = {}
 | |
|   var styleElement = document.createElement('style')
 | |
|   document.documentElement.getElementsByTagName('head')[0].appendChild(styleElement)
 | |
|   var styleSheet = styleElement.sheet
 | |
| 
 | |
|   return function (css) {
 | |
|     if (added[css]) {
 | |
|       return
 | |
|     }
 | |
|     added[css] = true
 | |
|     styleSheet.insertRule(css, (styleSheet.cssRules || styleSheet.rules).length)
 | |
|   }
 | |
| }())
 | |
| function updateAuthorshipInner () {
 | |
|     // ignore when ot not synced yet
 | |
|   if (havePendingOperation()) return
 | |
|   authorMarks = {}
 | |
|   for (let i = 0; i < authorship.length; i++) {
 | |
|     var atom = authorship[i]
 | |
|     let author = authors[atom[0]]
 | |
|     if (author) {
 | |
|       var prePos = editor.posFromIndex(atom[1])
 | |
|       var preLine = editor.getLine(prePos.line)
 | |
|       var postPos = editor.posFromIndex(atom[2])
 | |
|       var postLine = editor.getLine(postPos.line)
 | |
|       if (prePos.ch === 0 && postPos.ch === postLine.length) {
 | |
|         for (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) {
 | |
|         var startLine = prePos.line
 | |
|         var endLine = postPos.line
 | |
|         if (prePos.ch === preLine.length) {
 | |
|           startLine++
 | |
|         } else if (prePos.ch !== 0) {
 | |
|           let mark = initMarkAndCheckGutter(authorMarks[prePos.line], author, atom[3])
 | |
|           var _postPos = {
 | |
|             line: prePos.line,
 | |
|             ch: preLine.length
 | |
|           }
 | |
|           if (JSON.stringify(prePos) !== JSON.stringify(_postPos)) {
 | |
|             mark.textmarkers.push({
 | |
|               userid: author.userid,
 | |
|               pos: [prePos, _postPos]
 | |
|             })
 | |
|             startLine++
 | |
|           }
 | |
|           authorMarks[prePos.line] = mark
 | |
|         }
 | |
|         if (postPos.ch === 0) {
 | |
|           endLine--
 | |
|         } else if (postPos.ch !== postLine.length) {
 | |
|           let mark = initMarkAndCheckGutter(authorMarks[postPos.line], author, atom[3])
 | |
|           var _prePos = {
 | |
|             line: postPos.line,
 | |
|             ch: 0
 | |
|           }
 | |
|           if (JSON.stringify(_prePos) !== JSON.stringify(postPos)) {
 | |
|             mark.textmarkers.push({
 | |
|               userid: author.userid,
 | |
|               pos: [_prePos, postPos]
 | |
|             })
 | |
|             endLine--
 | |
|           }
 | |
|           authorMarks[postPos.line] = mark
 | |
|         }
 | |
|         for (let j = startLine; j <= endLine; j++) {
 | |
|           if (editor.getLine(j)) {
 | |
|             authorMarks[j] = initMarkAndCheckGutter(authorMarks[j], author, atom[3])
 | |
|           }
 | |
|         }
 | |
|       } else {
 | |
|         let mark = initMarkAndCheckGutter(authorMarks[prePos.line], author, atom[3])
 | |
|         if (JSON.stringify(prePos) !== JSON.stringify(postPos)) {
 | |
|           mark.textmarkers.push({
 | |
|             userid: author.userid,
 | |
|             pos: [prePos, postPos]
 | |
|           })
 | |
|         }
 | |
|         authorMarks[prePos.line] = mark
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   addTextMarkers = []
 | |
|   editor.eachLine(iterateLine)
 | |
|   var allTextMarks = editor.getAllMarks()
 | |
|   for (let i = 0; i < allTextMarks.length; i++) {
 | |
|     let _textMarker = allTextMarks[i]
 | |
|     var pos = _textMarker.find()
 | |
|     var found = false
 | |
|     for (let j = 0; j < addTextMarkers.length; j++) {
 | |
|       let textMarker = addTextMarkers[j]
 | |
|       let author = authors[textMarker.userid]
 | |
|       let className = 'authorship-inline-' + author.color.substr(1)
 | |
|       var obj = {
 | |
|         from: textMarker.pos[0],
 | |
|         to: textMarker.pos[1]
 | |
|       }
 | |
|       if (JSON.stringify(pos) === JSON.stringify(obj) && _textMarker.className &&
 | |
|                 _textMarker.className.indexOf(className) > -1) {
 | |
|         addTextMarkers.splice(j, 1)
 | |
|         j--
 | |
|         found = true
 | |
|         break
 | |
|       }
 | |
|     }
 | |
|     if (!found && _textMarker.className && _textMarker.className.indexOf('authorship-inline') > -1) {
 | |
|       _textMarker.clear()
 | |
|     }
 | |
|   }
 | |
|   for (let i = 0; i < addTextMarkers.length; i++) {
 | |
|     let textMarker = addTextMarkers[i]
 | |
|     let 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);`
 | |
|     let 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) {
 | |
|   var lineNumber = line.lineNo()
 | |
|   var currMark = authorMarks[lineNumber]
 | |
|   var author = currMark ? authors[currMark.gutter.userid] : null
 | |
|   if (currMark && author) {
 | |
|     let 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)
 | |
|       var gutter = $('<div>', {
 | |
|         class: 'authorship-gutter ' + className,
 | |
|         title: author.name
 | |
|       })
 | |
|       editor.setGutterMarker(line, 'authorship-gutters', gutter[0])
 | |
|     }
 | |
|   } else {
 | |
|     editor.setGutterMarker(line, 'authorship-gutters', null)
 | |
|   }
 | |
|   if (currMark && currMark.textmarkers.length > 0) {
 | |
|     for (var i = 0; i < currMark.textmarkers.length; i++) {
 | |
|       let 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) {
 | |
|     var $ele = $(element)
 | |
|     if ($('[aria-describedby="' + $ele.attr('id') + '"]').length <= 0) $ele.remove()
 | |
|   })
 | |
| })
 | |
| socket.on('check', function (data) {
 | |
|   // console.log(data);
 | |
|   updateInfo(data)
 | |
| })
 | |
| socket.on('permission', function (data) {
 | |
|   updatePermission(data.permission)
 | |
| })
 | |
| 
 | |
| var permission = null
 | |
| socket.on('refresh', function (data) {
 | |
|     // console.log(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
 | |
|     var nocontent = editor.getValue().length <= 0
 | |
|     if (nocontent) {
 | |
|       if (visibleXS) { window.currentMode = modeType.edit } else { window.currentMode = modeType.both }
 | |
|     }
 | |
|     // parse mode from url
 | |
|     if (window.location.search.length > 0) {
 | |
|       var urlMode = modeType[window.location.search.substr(1)]
 | |
|       if (urlMode) window.currentMode = urlMode
 | |
|     }
 | |
|     changeMode(window.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) }
 | |
| })
 | |
| 
 | |
| var EditorClient = ot.EditorClient
 | |
| var SocketIOAdapter = ot.SocketIOAdapter
 | |
| var CodeMirrorAdapter = ot.CodeMirrorAdapter
 | |
| var cmClient = null
 | |
| var synchronized_ = null
 | |
| 
 | |
| function havePendingOperation () {
 | |
|   return !!((cmClient && cmClient.state && cmClient.state.hasOwnProperty('outstanding')))
 | |
| }
 | |
| 
 | |
| socket.on('doc', function (obj) {
 | |
|   var body = obj.str
 | |
|   var bodyMismatch = editor.getValue() !== body
 | |
|   var 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) {
 | |
|     var found = false
 | |
|     for (var i = 0; i < data.users.length; i++) {
 | |
|       var user = data.users[i]
 | |
|       if ($(this).attr('id') === user.id) { found = true }
 | |
|     }
 | |
|     if (!found) {
 | |
|       $(this).stop(true).fadeOut('normal', function () {
 | |
|         $(this).remove()
 | |
|       })
 | |
|     }
 | |
|   })
 | |
|   for (var i = 0; i < data.users.length; i++) {
 | |
|     var user = data.users[i]
 | |
|     if (user.id !== socket.id) { buildCursor(user) } else { personalInfo = user }
 | |
|   }
 | |
| })
 | |
| socket.on('user status', function (data) {
 | |
|   if (debug) { console.debug(data) }
 | |
|   for (var i = 0; i < onlineUsers.length; i++) {
 | |
|     if (onlineUsers[i].id === data.id) {
 | |
|       onlineUsers[i] = data
 | |
|     }
 | |
|   }
 | |
|   updateOnlineStatus()
 | |
|   if (data.id !== socket.id) { buildCursor(data) }
 | |
| })
 | |
| socket.on('cursor focus', function (data) {
 | |
|   if (debug) { console.debug(data) }
 | |
|   for (var i = 0; i < onlineUsers.length; i++) {
 | |
|     if (onlineUsers[i].id === data.id) {
 | |
|       onlineUsers[i].cursor = data.cursor
 | |
|     }
 | |
|   }
 | |
|   if (data.id !== socket.id) { buildCursor(data) }
 | |
|     // force show
 | |
|   var cursor = $('div[data-clientid="' + data.id + '"]')
 | |
|   if (cursor.length > 0) {
 | |
|     cursor.stop(true).fadeIn()
 | |
|   }
 | |
| })
 | |
| socket.on('cursor activity', function (data) {
 | |
|   if (debug) { console.debug(data) }
 | |
|   for (var i = 0; i < onlineUsers.length; i++) {
 | |
|     if (onlineUsers[i].id === data.id) {
 | |
|       onlineUsers[i].cursor = data.cursor
 | |
|     }
 | |
|   }
 | |
|   if (data.id !== socket.id) { buildCursor(data) }
 | |
| })
 | |
| socket.on('cursor blur', function (data) {
 | |
|   if (debug) { console.debug(data) }
 | |
|   for (var i = 0; i < onlineUsers.length; i++) {
 | |
|     if (onlineUsers[i].id === data.id) {
 | |
|       onlineUsers[i].cursor = null
 | |
|     }
 | |
|   }
 | |
|   if (data.id !== socket.id) { buildCursor(data) }
 | |
|     // force hide
 | |
|   var cursor = $('div[data-clientid="' + data.id + '"]')
 | |
|   if (cursor.length > 0) {
 | |
|     cursor.stop(true).fadeOut()
 | |
|   }
 | |
| })
 | |
| 
 | |
| var options = {
 | |
|   valueNames: ['id', 'name'],
 | |
|   item: '<li class="ui-user-item">' +
 | |
|         '<span class="id" style="display:none;"></span>' +
 | |
|         '<a href="#">' +
 | |
|             '<span class="pull-left"><i class="ui-user-icon"></i></span><span class="ui-user-name name"></span><span class="pull-right"><i class="fa fa-circle ui-user-status"></i></span>' +
 | |
|         '</a>' +
 | |
|         '</li>'
 | |
| }
 | |
| var onlineUserList = new List('online-user-list', options)
 | |
| var shortOnlineUserList = new List('short-online-user-list', options)
 | |
| 
 | |
| function updateOnlineStatus () {
 | |
|   if (!window.loaded || !socket.connected) return
 | |
|   var _onlineUsers = deduplicateOnlineUsers(onlineUsers)
 | |
|   showStatus(statusType.online, _onlineUsers.length)
 | |
|   var 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
 | |
|       }
 | |
|     }
 | |
|     let 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) {
 | |
|       var usera = a.values()
 | |
|       var userb = b.values()
 | |
|       var useraIsSelf = (usera.id === personalInfo.id || (usera.login && usera.userid === personalInfo.userid))
 | |
|       var userbIsSelf = (userb.id === personalInfo.id || (userb.login && userb.userid === personalInfo.userid))
 | |
|       if (useraIsSelf && !userbIsSelf) {
 | |
|         return -1
 | |
|       } else if (!useraIsSelf && userbIsSelf) {
 | |
|         return 1
 | |
|       } else {
 | |
|         if (usera.login && !userb.login) { return -1 } else if (!usera.login && userb.login) { return 1 } else {
 | |
|           if (!usera.idle && userb.idle) { return -1 } else if (usera.idle && !userb.idle) { return 1 } else {
 | |
|             if (usera.name && 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) {
 | |
|   var items = list.items
 | |
|   for (var j = 0; j < items.length; j++) {
 | |
|     var item = items[j]
 | |
|     var userstatus = $(item.elm).find('.ui-user-status')
 | |
|     var usericon = $(item.elm).find('.ui-user-icon')
 | |
|     if (item.values().login && item.values().photo) {
 | |
|       usericon.css('background-image', 'url(' + item.values().photo + ')')
 | |
|             // add 1px more to right, make it feel aligned
 | |
|       usericon.css('margin-right', '6px')
 | |
|       $(item.elm).css('border-left', '4px solid ' + item.values().color)
 | |
|       usericon.css('margin-left', '-4px')
 | |
|     } else {
 | |
|       usericon.css('background-color', item.values().color)
 | |
|     }
 | |
|     userstatus.removeClass('ui-user-status-offline ui-user-status-online ui-user-status-idle')
 | |
|     if (item.values().idle) { userstatus.addClass('ui-user-status-idle') } else { userstatus.addClass('ui-user-status-online') }
 | |
|   }
 | |
| }
 | |
| 
 | |
| function deduplicateOnlineUsers (list) {
 | |
|   var _onlineUsers = []
 | |
|   for (var i = 0; i < list.length; i++) {
 | |
|     var user = $.extend({}, list[i])
 | |
|     if (!user.userid) { _onlineUsers.push(user) } else {
 | |
|       var found = false
 | |
|       for (var j = 0; j < _onlineUsers.length; j++) {
 | |
|         if (_onlineUsers[j].userid === user.userid) {
 | |
|           // keep self color when login
 | |
|           if (user.id === personalInfo.id) {
 | |
|             _onlineUsers[j].color = user.color
 | |
|           }
 | |
|           // keep idle state if any of self client not idle
 | |
|           if (!user.idle) {
 | |
|             _onlineUsers[j].idle = user.idle
 | |
|             _onlineUsers[j].color = user.color
 | |
|           }
 | |
|           found = true
 | |
|           break
 | |
|         }
 | |
|       }
 | |
|       if (!found) { _onlineUsers.push(user) }
 | |
|     }
 | |
|   }
 | |
|   return _onlineUsers
 | |
| }
 | |
| 
 | |
| var userStatusCache = null
 | |
| 
 | |
| function emitUserStatus (force) {
 | |
|   if (!window.loaded) return
 | |
|   var type = null
 | |
|   if (visibleXS) { type = 'xs' } else if (visibleSM) { type = 'sm' } else if (visibleMD) { type = 'md' } else if (visibleLG) { type = 'lg' }
 | |
| 
 | |
|   personalInfo['idle'] = idle.isAway
 | |
|   personalInfo['type'] = type
 | |
| 
 | |
|   for (var i = 0; i < onlineUsers.length; i++) {
 | |
|     if (onlineUsers[i].id === personalInfo.id) {
 | |
|       onlineUsers[i] = personalInfo
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   var userStatus = {
 | |
|     idle: idle.isAway,
 | |
|     type: type
 | |
|   }
 | |
| 
 | |
|   if (force || JSON.stringify(userStatus) !== JSON.stringify(userStatusCache)) {
 | |
|     socket.emit('user status', userStatus)
 | |
|     userStatusCache = userStatus
 | |
|   }
 | |
| }
 | |
| 
 | |
| function checkCursorTag (coord, ele) {
 | |
|   if (!ele) return // return if element not exists
 | |
|   // set margin
 | |
|   var tagRightMargin = 0
 | |
|   var tagBottomMargin = 2
 | |
|   // use sizer to get the real doc size (won't count status bar and gutters)
 | |
|   var docWidth = ui.area.codemirrorSizer.width()
 | |
|   // get editor size (status bar not count in)
 | |
|   var editorHeight = ui.area.codemirror.height()
 | |
|   // get element size
 | |
|   var width = ele.outerWidth()
 | |
|   var height = ele.outerHeight()
 | |
|   var padding = (ele.outerWidth() - ele.width()) / 2
 | |
|   // get coord position
 | |
|   var left = coord.left
 | |
|   var top = coord.top
 | |
|   // get doc top offset (to workaround with viewport)
 | |
|   var docTopOffset = ui.area.codemirrorSizerInner.position().top
 | |
|   // set offset
 | |
|   var offsetLeft = -3
 | |
|   var offsetTop = defaultTextHeight
 | |
|   // only do when have width and height
 | |
|   if (width > 0 && height > 0) {
 | |
|     // flip x when element right bound larger than doc width
 | |
|     if (left + width + offsetLeft + tagRightMargin > docWidth) {
 | |
|       offsetLeft = -(width + tagRightMargin) + padding + offsetLeft
 | |
|     }
 | |
|     // flip y when element bottom bound larger than doc height
 | |
|     // and element top position is larger than element height
 | |
|     if (top + docTopOffset + height + offsetTop + tagBottomMargin > Math.max(editor.doc.height, editorHeight) && top + docTopOffset > height + tagBottomMargin) {
 | |
|       offsetTop = -(height)
 | |
|     }
 | |
|   }
 | |
|   // set position
 | |
|   ele[0].style.left = offsetLeft + 'px'
 | |
|   ele[0].style.top = offsetTop + 'px'
 | |
| }
 | |
| 
 | |
| function buildCursor (user) {
 | |
|   if (window.currentMode === modeType.view) return
 | |
|   if (!user.cursor) return
 | |
|   var coord = editor.charCoords(user.cursor, 'windows')
 | |
|   coord.left = coord.left < 4 ? 4 : coord.left
 | |
|   coord.top = coord.top < 0 ? 0 : coord.top
 | |
|   var iconClass = 'fa-user'
 | |
|   switch (user.type) {
 | |
|     case 'xs':
 | |
|       iconClass = 'fa-mobile'
 | |
|       break
 | |
|     case 'sm':
 | |
|       iconClass = 'fa-tablet'
 | |
|       break
 | |
|     case 'md':
 | |
|       iconClass = 'fa-desktop'
 | |
|       break
 | |
|     case 'lg':
 | |
|       iconClass = 'fa-desktop'
 | |
|       break
 | |
|   }
 | |
|   if ($('.CodeMirror-other-cursors').length <= 0) {
 | |
|     $("<div class='CodeMirror-other-cursors'>").insertAfter('.CodeMirror-cursors')
 | |
|   }
 | |
|   if ($('div[data-clientid="' + user.id + '"]').length <= 0) {
 | |
|     let 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)
 | |
| 
 | |
|     let cursorbar = $('<div class="cursorbar"> </div>')
 | |
|     cursorbar[0].style.height = defaultTextHeight + 'px'
 | |
|     cursorbar[0].style.borderLeft = '2px solid ' + user.color
 | |
| 
 | |
|     var icon = '<i class="fa ' + iconClass + '"></i>'
 | |
| 
 | |
|     let 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') }
 | |
|       })
 | |
| 
 | |
|     var hideCursorTagDelay = 2000
 | |
|     var hideCursorTagTimer = null
 | |
| 
 | |
|     var 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') }
 | |
|     }
 | |
| 
 | |
|     var switchTag = function (ele) {
 | |
|       if (ele.css('display') === 'none') { ele.stop(true).fadeIn('fast') } else { ele.stop(true).fadeOut('fast') }
 | |
|     }
 | |
| 
 | |
|     var hideCursorTag = function () {
 | |
|       if (cursor.attr('data-mode') === 'hover') { cursortag.fadeOut('fast') }
 | |
|     }
 | |
|     cursor.on('touchstart', function (e) {
 | |
|       var display = cursortag.css('display')
 | |
|       cursortag.stop(true).fadeIn('fast')
 | |
|       clearTimeout(hideCursorTagTimer)
 | |
|       hideCursorTagTimer = setTimeout(hideCursorTag, hideCursorTagDelay)
 | |
|       if (display === 'none') {
 | |
|         e.preventDefault()
 | |
|         e.stopPropagation()
 | |
|       }
 | |
|     })
 | |
|     cursortag.on('mousedown touchstart', function (e) {
 | |
|       if (cursor.attr('data-mode') === 'state') { switchTag(cursortag) }
 | |
|       switchMode(cursor)
 | |
|       e.preventDefault()
 | |
|       e.stopPropagation()
 | |
|     })
 | |
| 
 | |
|     cursor.append(cursorbar)
 | |
|     cursor.append(cursortag)
 | |
| 
 | |
|     cursor[0].style.left = coord.left + 'px'
 | |
|     cursor[0].style.top = coord.top + 'px'
 | |
|     $('.CodeMirror-other-cursors').append(cursor)
 | |
| 
 | |
|     if (!user.idle) { cursor.stop(true).fadeIn() }
 | |
| 
 | |
|     checkCursorTag(coord, cursortag)
 | |
|   } else {
 | |
|     let cursor = $('div[data-clientid="' + user.id + '"]')
 | |
|     cursor.attr('data-line', user.cursor.line)
 | |
|     cursor.attr('data-ch', user.cursor.ch)
 | |
| 
 | |
|     let cursorbar = cursor.find('.cursorbar')
 | |
|     cursorbar[0].style.height = defaultTextHeight + 'px'
 | |
|     cursorbar[0].style.borderLeft = '2px solid ' + user.color
 | |
| 
 | |
|     let 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) {
 | |
|   var str = change.text.join('\n')
 | |
|   if (/\u0000/g.test(str) && change.update) {
 | |
|     change.update(change.from, change.to, str.replace(/\u0000/g, '').split('\n'))
 | |
|   }
 | |
| }
 | |
| function enforceMaxLength (cm, change) {
 | |
|   var maxLength = cm.getOption('maxLength')
 | |
|   if (maxLength && change.update) {
 | |
|     var str = change.text.join('\n')
 | |
|     var delta = str.length - (cm.indexFromPos(change.to) - cm.indexFromPos(change.from))
 | |
|     if (delta <= 0) {
 | |
|       return false
 | |
|     }
 | |
|     delta = cm.getValue().length + delta - maxLength
 | |
|     if (delta > 0) {
 | |
|       str = str.substr(0, str.length - delta)
 | |
|       change.update(change.from, change.to, str.split('\n'))
 | |
|       return true
 | |
|     }
 | |
|   }
 | |
|   return false
 | |
| }
 | |
| var ignoreEmitEvents = ['setValue', 'ignoreHistory']
 | |
| editorInstance.on('beforeChange', function (cm, change) {
 | |
|   if (debug) { console.debug(change) }
 | |
|   removeNullByte(cm, change)
 | |
|   if (enforceMaxLength(cm, change)) {
 | |
|     $('.limit-modal').modal('show')
 | |
|   }
 | |
|   var isIgnoreEmitEvent = (ignoreEmitEvents.indexOf(change.origin) !== -1)
 | |
|   if (!isIgnoreEmitEvent) {
 | |
|     if (!havePermission()) {
 | |
|       change.canceled = true
 | |
|       switch (permission) {
 | |
|         case 'editable':
 | |
|           $('.signin-modal').modal('show')
 | |
|           break
 | |
|         case 'locked':
 | |
|         case 'private':
 | |
|           $('.locked-modal').modal('show')
 | |
|           break
 | |
|       }
 | |
|     }
 | |
|   } else {
 | |
|     if (change.origin === 'ignoreHistory') {
 | |
|       setHaveUnreadChanges(true)
 | |
|       updateTitleReminder()
 | |
|     }
 | |
|   }
 | |
|   if (cmClient && !socket.connected) { cmClient.editorAdapter.ignoreNextChange = true }
 | |
| })
 | |
| editorInstance.on('cut', function () {
 | |
|     // na
 | |
| })
 | |
| editorInstance.on('paste', function () {
 | |
|     // na
 | |
| })
 | |
| editorInstance.on('changes', function (editor, changes) {
 | |
|   updateHistory()
 | |
|   var docLength = editor.getValue().length
 | |
|     // workaround for big documents
 | |
|   var newViewportMargin = 20
 | |
|   if (docLength > 20000) {
 | |
|     newViewportMargin = 1
 | |
|   } else if (docLength > 10000) {
 | |
|     newViewportMargin = 10
 | |
|   } else if (docLength > 5000) {
 | |
|     newViewportMargin = 15
 | |
|   }
 | |
|   if (newViewportMargin !== viewportMargin) {
 | |
|     viewportMargin = newViewportMargin
 | |
|     windowResize()
 | |
|   }
 | |
|   checkEditorScrollbar()
 | |
|   if (ui.area.codemirrorScroll[0].scrollHeight > ui.area.view[0].scrollHeight && editorHasFocus()) {
 | |
|     postUpdateEvent = function () {
 | |
|       syncScrollToView()
 | |
|       postUpdateEvent = null
 | |
|     }
 | |
|   }
 | |
| })
 | |
| editorInstance.on('focus', function (editor) {
 | |
|   for (var i = 0; i < onlineUsers.length; i++) {
 | |
|     if (onlineUsers[i].id === personalInfo.id) {
 | |
|       onlineUsers[i].cursor = editor.getCursor()
 | |
|     }
 | |
|   }
 | |
|   personalInfo['cursor'] = editor.getCursor()
 | |
|   socket.emit('cursor focus', editor.getCursor())
 | |
| })
 | |
| 
 | |
| const cursorActivity = _.debounce(cursorActivityInner, cursorActivityDebounce)
 | |
| 
 | |
| function cursorActivityInner (editor) {
 | |
|   if (editorHasFocus() && !Visibility.hidden()) {
 | |
|     for (var i = 0; i < onlineUsers.length; i++) {
 | |
|       if (onlineUsers[i].id === personalInfo.id) {
 | |
|         onlineUsers[i].cursor = editor.getCursor()
 | |
|       }
 | |
|     }
 | |
|     personalInfo['cursor'] = editor.getCursor()
 | |
|     socket.emit('cursor activity', editor.getCursor())
 | |
|   }
 | |
| }
 | |
| 
 | |
| 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) {
 | |
|       var lines = end.line - start.line + 1
 | |
|       if (end.ch === 0) {
 | |
|         lines--
 | |
|       }
 | |
|       selectionText += lines + ' lines'
 | |
|     } else if (selectionCharCount > 0) {
 | |
|       selectionText += selectionCharCount + ' columns'
 | |
|     }
 | |
| 
 | |
|     if (start.line !== end.line || selectionCharCount > 0) {
 | |
|       editorInstance.statusSelection.text(selectionText)
 | |
|     } else {
 | |
|       editorInstance.statusSelection.text('')
 | |
|     }
 | |
|   }
 | |
| })
 | |
| 
 | |
| editorInstance.on('blur', function (cm) {
 | |
|   for (var i = 0; i < onlineUsers.length; i++) {
 | |
|     if (onlineUsers[i].id === personalInfo.id) {
 | |
|       onlineUsers[i].cursor = null
 | |
|     }
 | |
|   }
 | |
|   personalInfo['cursor'] = null
 | |
|   socket.emit('cursor blur')
 | |
| })
 | |
| 
 | |
| function saveInfo () {
 | |
|   var scrollbarStyle = editor.getOption('scrollbarStyle')
 | |
|   var left = $(window).scrollLeft()
 | |
|   var top = $(window).scrollTop()
 | |
|   switch (window.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 () {
 | |
|   var scrollbarStyle = editor.getOption('scrollbarStyle')
 | |
|   if (lastInfo.needRestore) {
 | |
|     var line = lastInfo.edit.cursor.line
 | |
|     var ch = lastInfo.edit.cursor.ch
 | |
|     editor.setCursor(line, ch)
 | |
|     editor.setSelections(lastInfo.edit.selections)
 | |
|     switch (window.currentMode) {
 | |
|       case modeType.edit:
 | |
|         if (scrollbarStyle === 'native') {
 | |
|           $(window).scrollLeft(lastInfo.edit.scroll.left)
 | |
|           $(window).scrollTop(lastInfo.edit.scroll.top)
 | |
|         } else {
 | |
|           let left = lastInfo.edit.scroll.left
 | |
|           let 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:
 | |
|         let left = lastInfo.edit.scroll.left
 | |
|         let top = lastInfo.edit.scroll.top
 | |
|         editor.scrollIntoView()
 | |
|         editor.scrollTo(left, top)
 | |
|         ui.area.view.scrollLeft(lastInfo.view.scroll.left)
 | |
|         ui.area.view.scrollTop(lastInfo.view.scroll.top)
 | |
|         break
 | |
|     }
 | |
| 
 | |
|     lastInfo.needRestore = false
 | |
|   }
 | |
| }
 | |
| 
 | |
| // view actions
 | |
| function refreshView () {
 | |
|   ui.area.markdown.html('')
 | |
|   isDirty = true
 | |
|   updateViewInner()
 | |
| }
 | |
| 
 | |
| var updateView = _.debounce(function () {
 | |
|   editor.operation(updateViewInner)
 | |
| }, updateViewDebounce)
 | |
| 
 | |
| var lastResult = null
 | |
| var postUpdateEvent = null
 | |
| 
 | |
| function updateViewInner () {
 | |
|   if (window.currentMode === modeType.edit || !isDirty) return
 | |
|   var value = editor.getValue()
 | |
|   var lastMeta = md.meta
 | |
|   md.meta = {}
 | |
|   delete md.metaError
 | |
|   var rendered = md.render(value)
 | |
|   if (md.meta.type && md.meta.type === 'slide') {
 | |
|     var slideOptions = {
 | |
|       separator: '^(\r\n?|\n)---(\r\n?|\n)$',
 | |
|       verticalSeparator: '^(\r\n?|\n)----(\r\n?|\n)$'
 | |
|     }
 | |
|     var slides = 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')
 | |
|     window.syncscroll = false
 | |
|     checkSyncToggle()
 | |
|   } else {
 | |
|     if (lastMeta.type && lastMeta.type === 'slide') {
 | |
|       refreshView()
 | |
|       ui.area.markdown.removeClass('slides')
 | |
|       window.syncscroll = true
 | |
|       checkSyncToggle()
 | |
|     }
 | |
|         // only render again when meta changed
 | |
|     if (JSON.stringify(md.meta) !== JSON.stringify(lastMeta)) {
 | |
|       parseMeta(md, ui.area.codemirror, ui.area.markdown, $('#ui-toc'), $('#ui-toc-affix'))
 | |
|       rendered = md.render(value)
 | |
|     }
 | |
|         // prevent XSS
 | |
|     rendered = preventXSS(rendered)
 | |
|     var result = postProcess(rendered).children().toArray()
 | |
|     partialUpdate(result, lastResult, ui.area.markdown.children().toArray())
 | |
|     if (result && lastResult && result.length !== lastResult.length) { updateDataAttrs(result, ui.area.markdown.children().toArray()) }
 | |
|     lastResult = $(result).clone()
 | |
|   }
 | |
|   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')
 | |
|   generateScrollspy()
 | |
|   updateScrollspy()
 | |
|   smoothHashScroll()
 | |
|   isDirty = false
 | |
|   clearMap()
 | |
|     // buildMap();
 | |
|   updateTitleReminder()
 | |
|   if (postUpdateEvent && typeof postUpdateEvent === 'function') { postUpdateEvent() }
 | |
| }
 | |
| 
 | |
| var updateHistoryDebounce = 600
 | |
| 
 | |
| var updateHistory = _.debounce(updateHistoryInner, updateHistoryDebounce)
 | |
| 
 | |
| function updateHistoryInner () {
 | |
|   writeHistory(renderFilename(ui.area.markdown), renderTags(ui.area.markdown))
 | |
| }
 | |
| 
 | |
| function updateDataAttrs (src, des) {
 | |
|     // sync data attr startline and endline
 | |
|   for (var i = 0; i < src.length; i++) {
 | |
|     copyAttribute(src[i], des[i], 'data-startline')
 | |
|     copyAttribute(src[i], des[i], 'data-endline')
 | |
|   }
 | |
| }
 | |
| 
 | |
| function partialUpdate (src, tar, des) {
 | |
|   if (!src || src.length === 0 || !tar || tar.length === 0 || !des || des.length === 0) {
 | |
|     ui.area.markdown.html(src)
 | |
|     return
 | |
|   }
 | |
|   if (src.length === tar.length) { // same length
 | |
|     for (let i = 0; i < src.length; i++) {
 | |
|       copyAttribute(src[i], des[i], 'data-startline')
 | |
|       copyAttribute(src[i], des[i], 'data-endline')
 | |
|       var rawSrc = cloneAndRemoveDataAttr(src[i])
 | |
|       var rawTar = cloneAndRemoveDataAttr(tar[i])
 | |
|       if (rawSrc.outerHTML !== rawTar.outerHTML) {
 | |
|                 // console.log(rawSrc);
 | |
|                 // console.log(rawTar);
 | |
|         $(des[i]).replaceWith(src[i])
 | |
|       }
 | |
|     }
 | |
|   } else { // diff length
 | |
|     var start = 0
 | |
|     // 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');
 | |
|       let rawSrc = cloneAndRemoveDataAttr(src[i])
 | |
|       let rawTar = cloneAndRemoveDataAttr(tar[i])
 | |
|       if (!rawSrc || !rawTar || rawSrc.outerHTML !== rawTar.outerHTML) {
 | |
|         start = i
 | |
|         break
 | |
|       }
 | |
|     }
 | |
|         // find diff end position
 | |
|     var srcEnd = 0
 | |
|     var tarEnd = 0
 | |
|     for (let i = 0; i < src.length; i++) {
 | |
|             // copyAttribute(src[i], des[i], 'data-startline');
 | |
|             // copyAttribute(src[i], des[i], 'data-endline');
 | |
|       let rawSrc = cloneAndRemoveDataAttr(src[i])
 | |
|       let 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++) {
 | |
|       let srcLength = src.length
 | |
|       let tarLength = tar.length
 | |
|             // copyAttribute(src[srcLength - i], des[srcLength - i], 'data-startline');
 | |
|             // copyAttribute(src[srcLength - i], des[srcLength - i], 'data-endline');
 | |
|       let rawSrc = cloneAndRemoveDataAttr(src[srcLength - i])
 | |
|       let 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++) {
 | |
|       let srcLength = src.length
 | |
|       let tarLength = tar.length
 | |
|             // copyAttribute(src[srcLength - i], des[srcLength - i], 'data-startline');
 | |
|             // copyAttribute(src[srcLength - i], des[srcLength - i], 'data-endline');
 | |
|       let rawSrc = cloneAndRemoveDataAttr(src[srcLength - i])
 | |
|       let rawTar = cloneAndRemoveDataAttr(tar[tarLength - i])
 | |
|       if (!rawSrc || !rawTar || rawSrc.outerHTML !== rawTar.outerHTML) {
 | |
|         srcEnd = src.length - i
 | |
|         break
 | |
|       }
 | |
|     }
 | |
|         // check if tar end overlap tar start
 | |
|     var overlap = 0
 | |
|     for (var i = start; i >= 0; i--) {
 | |
|       var rawTarStart = cloneAndRemoveDataAttr(tar[i - 1])
 | |
|       var rawTarEnd = cloneAndRemoveDataAttr(tar[tarEnd + 1 + start - i])
 | |
|       if (rawTarStart && rawTarEnd && rawTarStart.outerHTML === rawTarEnd.outerHTML) { overlap++ } else { break }
 | |
|     }
 | |
|     if (debug) { console.log('overlap:' + overlap) }
 | |
|         // show diff content
 | |
|     if (debug) {
 | |
|       console.log('start:' + start)
 | |
|       console.log('tarEnd:' + tarEnd)
 | |
|       console.log('srcEnd:' + srcEnd)
 | |
|     }
 | |
|     tarEnd += overlap
 | |
|     srcEnd += overlap
 | |
|     var repeatAdd = (start - srcEnd) < (start - tarEnd)
 | |
|     var repeatDiff = Math.abs(srcEnd - tarEnd) - 1
 | |
|         // push new elements
 | |
|     var newElements = []
 | |
|     if (srcEnd >= start) {
 | |
|       for (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
 | |
|     var 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.log('ADD ELEMENTS')
 | |
|       console.log(newElements.join('\n'))
 | |
|     }
 | |
|     if (des[start]) { $(newElements.join('')).insertBefore(des[start]) } else { $(newElements.join('')).insertAfter(des[start - 1]) }
 | |
|         // remove elements
 | |
|     if (debug) { console.log('REMOVE ELEMENTS') }
 | |
|     for (let j = 0; j < removeElements.length; j++) {
 | |
|       if (debug) {
 | |
|         console.log(removeElements[j].outerHTML)
 | |
|       }
 | |
|       if (removeElements[j]) { $(removeElements[j]).remove() }
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| function cloneAndRemoveDataAttr (el) {
 | |
|   if (!el) return
 | |
|   var rawEl = $(el).clone()
 | |
|   rawEl.removeAttr('data-startline data-endline')
 | |
|   rawEl.find('[data-startline]').removeAttr('data-startline data-endline')
 | |
|   return rawEl[0]
 | |
| }
 | |
| 
 | |
| function copyAttribute (src, des, attr) {
 | |
|   if (src && src.getAttribute(attr) && des) { des.setAttribute(attr, src.getAttribute(attr)) }
 | |
| }
 | |
| 
 | |
| if ($('.cursor-menu').length <= 0) {
 | |
|   $("<div class='cursor-menu'>").insertAfter('.CodeMirror-cursors')
 | |
| }
 | |
| 
 | |
| function reverseSortCursorMenu (dropdown) {
 | |
|   var items = dropdown.find('.textcomplete-item')
 | |
|   items.sort(function (a, b) {
 | |
|     return $(b).attr('data-index') - $(a).attr('data-index')
 | |
|   })
 | |
|   return items
 | |
| }
 | |
| 
 | |
| var checkCursorMenu = _.throttle(checkCursorMenuInner, cursorMenuThrottle)
 | |
| 
 | |
| function checkCursorMenuInner () {
 | |
|     // get element
 | |
|   var dropdown = $('.cursor-menu > .dropdown-menu')
 | |
|     // return if not exists
 | |
|   if (dropdown.length <= 0) return
 | |
|     // set margin
 | |
|   var menuRightMargin = 10
 | |
|   var menuBottomMargin = 4
 | |
|     // use sizer to get the real doc size (won't count status bar and gutters)
 | |
|   var docWidth = ui.area.codemirrorSizer.width()
 | |
|     // get editor size (status bar not count in)
 | |
|   var editorHeight = ui.area.codemirror.height()
 | |
|     // get element size
 | |
|   var width = dropdown.outerWidth()
 | |
|   var height = dropdown.outerHeight()
 | |
|     // get cursor
 | |
|   var cursor = editor.getCursor()
 | |
|     // set element cursor data
 | |
|   if (!dropdown.hasClass('CodeMirror-other-cursor')) { dropdown.addClass('CodeMirror-other-cursor') }
 | |
|   dropdown.attr('data-line', cursor.line)
 | |
|   dropdown.attr('data-ch', cursor.ch)
 | |
|     // get coord position
 | |
|   var coord = editor.charCoords({
 | |
|     line: cursor.line,
 | |
|     ch: cursor.ch
 | |
|   }, 'windows')
 | |
|   var left = coord.left
 | |
|   var top = coord.top
 | |
|     // get doc top offset (to workaround with viewport)
 | |
|   var docTopOffset = ui.area.codemirrorSizerInner.position().top
 | |
|     // set offset
 | |
|   var offsetLeft = 0
 | |
|   var offsetTop = defaultTextHeight
 | |
|     // set up side down
 | |
|   window.upSideDown = false
 | |
|   var 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
 | |
|     }
 | |
|     var 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
 | |
|   var line = editor.getLine(editor.getCursor().line)
 | |
|   var isIndentCode = ((line.substr(0, 4) === '    ') || (line.substr(0, 1) === '\t'))
 | |
|   return isIndentCode
 | |
| }
 | |
| 
 | |
| var isInCode = false
 | |
| 
 | |
| function checkInCode () {
 | |
|   isInCode = checkAbove(matchInCode) || checkInIndentCode()
 | |
| }
 | |
| 
 | |
| function checkAbove (method) {
 | |
|   var cursor = editor.getCursor()
 | |
|   var text = []
 | |
|   for (var i = 0; i < cursor.line; i++) {  // contain current line
 | |
|     text.push(editor.getLine(i))
 | |
|   }
 | |
|   text = text.join('\n') + '\n' + editor.getLine(cursor.line).slice(0, cursor.ch)
 | |
|     // console.log(text);
 | |
|   return method(text)
 | |
| }
 | |
| 
 | |
| function checkBelow (method) {
 | |
|   var cursor = editor.getCursor()
 | |
|   var count = editor.lineCount()
 | |
|   var text = []
 | |
|   for (var i = cursor.line + 1; i < count; i++) { // contain current line
 | |
|     text.push(editor.getLine(i))
 | |
|   }
 | |
|   text = editor.getLine(cursor.line).slice(cursor.ch) + '\n' + text.join('\n')
 | |
|     // console.log(text);
 | |
|   return method(text)
 | |
| }
 | |
| 
 | |
| function matchInCode (text) {
 | |
|   var match
 | |
|   match = text.match(/`{3,}/g)
 | |
|   if (match && match.length % 2) {
 | |
|     return true
 | |
|   } else {
 | |
|     match = text.match(/`/g)
 | |
|     if (match && match.length % 2) {
 | |
|       return true
 | |
|     } else {
 | |
|       return false
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| var isInContainer = false
 | |
| var isInContainerSyntax = false
 | |
| 
 | |
| function checkInContainer () {
 | |
|   isInContainer = checkAbove(matchInContainer) && !checkInIndentCode()
 | |
| }
 | |
| 
 | |
| function checkInContainerSyntax () {
 | |
|     // if line starts with :::, it's in container syntax
 | |
|   var line = editor.getLine(editor.getCursor().line)
 | |
|   isInContainerSyntax = (line.substr(0, 3) === ':::')
 | |
| }
 | |
| 
 | |
| function matchInContainer (text) {
 | |
|   var match
 | |
|   match = text.match(/:{3,}/g)
 | |
|   if (match && match.length % 2) {
 | |
|     return true
 | |
|   } else {
 | |
|     return false
 | |
|   }
 | |
| }
 | |
| 
 | |
| $(editor.getInputField())
 | |
|     .textcomplete([
 | |
|       { // emoji strategy
 | |
|         match: /(^|\n|\s)\B:([-+\w]*)$/,
 | |
|         search: function (term, callback) {
 | |
|           var line = editor.getLine(editor.getCursor().line)
 | |
|           term = line.match(this.match)[2]
 | |
|           var list = []
 | |
|           $.map(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) {
 | |
|           var line = editor.getLine(editor.getCursor().line)
 | |
|           term = line.match(this.match)[2]
 | |
|           var list = []
 | |
|           $.map(this.langs, function (lang) {
 | |
|             if (lang.indexOf(term) === 0 && lang !== term) { list.push(lang) }
 | |
|           })
 | |
|           $.map(this.charts, function (chart) {
 | |
|             if (chart.indexOf(term) === 0 && chart !== term) { list.push(chart) }
 | |
|           })
 | |
|           callback(list)
 | |
|         },
 | |
|         replace: function (lang) {
 | |
|           var ending = ''
 | |
|           if (!checkBelow(matchInCode)) {
 | |
|             ending = '\n\n```'
 | |
|           }
 | |
|           if (this.langs.indexOf(lang) !== -1) { return '$1```' + lang + '=' + ending } else if (this.charts.indexOf(lang) !== -1) { return '$1```' + lang + ending }
 | |
|         },
 | |
|         done: function () {
 | |
|           var cursor = editor.getCursor()
 | |
|           var text = []
 | |
|           text.push(editor.getLine(cursor.line - 1))
 | |
|           text.push(editor.getLine(cursor.line))
 | |
|           text = text.join('\n')
 | |
|                 // console.log(text);
 | |
|           if (text === '\n```') { editor.doc.cm.execCommand('goLineUp') }
 | |
|         },
 | |
|         context: function (text) {
 | |
|           return isInCode
 | |
|         }
 | |
|       },
 | |
|       { // Container strategy
 | |
|         containers: supportContainers,
 | |
|         match: /(^|\n):::(\s*)(\w*)$/,
 | |
|         search: function (term, callback) {
 | |
|           var line = editor.getLine(editor.getCursor().line)
 | |
|           term = line.match(this.match)[3].trim()
 | |
|           var list = []
 | |
|           $.map(this.containers, function (container) {
 | |
|             if (container.indexOf(term) === 0 && container !== term) { list.push(container) }
 | |
|           })
 | |
|           callback(list)
 | |
|         },
 | |
|         replace: function (lang) {
 | |
|           var ending = ''
 | |
|           if (!checkBelow(matchInContainer)) {
 | |
|             ending = '\n\n:::'
 | |
|           }
 | |
|           if (this.containers.indexOf(lang) !== -1) { return '$1:::$2' + lang + ending }
 | |
|         },
 | |
|         done: function () {
 | |
|           var cursor = editor.getCursor()
 | |
|           var text = []
 | |
|           text.push(editor.getLine(cursor.line - 1))
 | |
|           text.push(editor.getLine(cursor.line))
 | |
|           text = text.join('\n')
 | |
|                 // console.log(text);
 | |
|           if (text === '\n:::') { editor.doc.cm.execCommand('goLineUp') }
 | |
|         },
 | |
|         context: function (text) {
 | |
|           return !isInCode && isInContainer
 | |
|         }
 | |
|       },
 | |
|       { // header
 | |
|         match: /(?:^|\n)(\s{0,3})(#{1,6}\w*)$/,
 | |
|         search: function (term, callback) {
 | |
|           callback($.map(supportHeaders, function (header) {
 | |
|             return header.search.indexOf(term) === 0 ? header.text : null
 | |
|           }))
 | |
|         },
 | |
|         replace: function (value) {
 | |
|           return '$1' + value
 | |
|         },
 | |
|         context: function (text) {
 | |
|           return !isInCode
 | |
|         }
 | |
|       },
 | |
|       { // extra tags for list
 | |
|         match: /(^[>\s]*[-+*]\s(?:\[[x ]\]|.*))(\[\])(\w*)$/,
 | |
|         search: function (term, callback) {
 | |
|           var list = []
 | |
|           $.map(supportExtraTags, function (extratag) {
 | |
|             if (extratag.search.indexOf(term) === 0) { list.push(extratag.command()) }
 | |
|           })
 | |
|           $.map(supportReferrals, function (referral) {
 | |
|             if (referral.search.indexOf(term) === 0) { list.push(referral.text) }
 | |
|           })
 | |
|           callback(list)
 | |
|         },
 | |
|         replace: function (value) {
 | |
|           return '$1' + value
 | |
|         },
 | |
|         context: function (text) {
 | |
|           return !isInCode
 | |
|         }
 | |
|       },
 | |
|       { // extra tags for blockquote
 | |
|         match: /(?:^|\n|\s)(>.*|\s|)((\^|)\[(\^|)\](\[\]|\(\)|:|)\s*\w*)$/,
 | |
|         search: function (term, callback) {
 | |
|           var line = editor.getLine(editor.getCursor().line)
 | |
|           var quote = line.match(this.match)[1].trim()
 | |
|           var list = []
 | |
|           if (quote.indexOf('>') === 0) {
 | |
|             $.map(supportExtraTags, function (extratag) {
 | |
|               if (extratag.search.indexOf(term) === 0) { list.push(extratag.command()) }
 | |
|             })
 | |
|           }
 | |
|           $.map(supportReferrals, function (referral) {
 | |
|             if (referral.search.indexOf(term) === 0) { list.push(referral.text) }
 | |
|           })
 | |
|           callback(list)
 | |
|         },
 | |
|         replace: function (value) {
 | |
|           return '$1' + value
 | |
|         },
 | |
|         context: function (text) {
 | |
|           return !isInCode
 | |
|         }
 | |
|       },
 | |
|       { // 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)
 | |
|       }
 | |
|     })
 |