8b69013ebd
The night mode toggle doesn't get the right state after restore from local storage. This results in the need to toggle twice to disable night mode. This patch adds the needed class so the toggleNightMode function gets the right state on execution. Signed-off-by: Sheogorath <sheogorath@shivering-isles.com>
3424 lines
105 KiB
JavaScript
3424 lines
105 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 store from 'store'
|
|
|
|
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 './lib/syncscroll'
|
|
|
|
import {
|
|
writeHistory,
|
|
deleteServerHistory,
|
|
getHistory,
|
|
saveHistory,
|
|
removeHistory
|
|
} from './history'
|
|
|
|
import { preventXSS } from './render'
|
|
|
|
import Editor from './lib/editor'
|
|
|
|
import getUIElements from './lib/editor/ui-elements'
|
|
import modeType from './lib/modeType'
|
|
import appState from './lib/appState'
|
|
|
|
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: '![image alt](https:// "title")',
|
|
search: '![]()'
|
|
},
|
|
{
|
|
text: '![image alt](https:// "title" =WidthxHeight)',
|
|
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'
|
|
}
|
|
}
|
|
|
|
// global vars
|
|
window.loaded = false
|
|
let needRefresh = false
|
|
let isDirty = false
|
|
let editShown = false
|
|
let visibleXS = false
|
|
let visibleSM = false
|
|
let visibleMD = false
|
|
let visibleLG = false
|
|
const isTouchDevice = 'ontouchstart' in document.documentElement
|
|
let currentStatus = statusType.offline
|
|
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()
|
|
|
|
/* cache dom references */
|
|
var $body = $('body')
|
|
|
|
/* we need this only on touch devices */
|
|
if (isTouchDevice) {
|
|
/* bind events */
|
|
$(document)
|
|
.on('focus', 'textarea, input', function () {
|
|
$body.addClass('fixfixed')
|
|
})
|
|
.on('blur', 'textarea, input', function () {
|
|
$body.removeClass('fixfixed')
|
|
})
|
|
}
|
|
|
|
// Re-enable nightmode
|
|
if (store.get('nightMode') || Cookies.get('nightMode')) {
|
|
$body.addClass('night')
|
|
ui.toolbar.night.addClass('active')
|
|
}
|
|
|
|
// showup
|
|
$().showUp('.navbar', {
|
|
upClass: 'navbar-hide',
|
|
downClass: 'navbar-show'
|
|
})
|
|
// tooltip
|
|
$('[data-toggle="tooltip"]').tooltip()
|
|
// shortcuts
|
|
// allow on all tags
|
|
key.filter = function (e) { return true }
|
|
key('ctrl+alt+e', function (e) {
|
|
changeMode(modeType.edit)
|
|
})
|
|
key('ctrl+alt+v', function (e) {
|
|
changeMode(modeType.view)
|
|
})
|
|
key('ctrl+alt+b', function (e) {
|
|
changeMode(modeType.both)
|
|
})
|
|
// toggle-dropdown
|
|
$(document).on('click', '.toggle-dropdown .dropdown-menu', function (e) {
|
|
e.stopPropagation()
|
|
})
|
|
})
|
|
// when page resize
|
|
$(window).resize(function () {
|
|
checkLayout()
|
|
checkEditorStyle()
|
|
checkTocStyle()
|
|
checkCursorMenu()
|
|
windowResize()
|
|
})
|
|
// when page unload
|
|
$(window).on('unload', function () {
|
|
// updateHistoryInner();
|
|
})
|
|
$(window).on('error', function () {
|
|
// setNeedRefresh();
|
|
})
|
|
|
|
setupSyncAreas(ui.area.codemirrorScroll, ui.area.view, ui.area.markdown, editor)
|
|
|
|
function autoSyncscroll () {
|
|
if (editorHasFocus()) {
|
|
syncScrollToView()
|
|
} else {
|
|
syncScrollToEdit()
|
|
}
|
|
}
|
|
|
|
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 && appState.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' || appState.currentMode === modeType.both) {
|
|
ui.area.codemirrorScroll.css('height', desireHeight + 'px')
|
|
ui.area.codemirrorScroll.css('min-height', '')
|
|
checkEditorScrollbar()
|
|
} else if (scrollbarStyle === 'native') {
|
|
ui.area.codemirrorScroll.css('height', '')
|
|
ui.area.codemirrorScroll.css('min-height', desireHeight + 'px')
|
|
}
|
|
// workaround editor will have wrong doc height when editor height changed
|
|
editor.setSize(null, ui.area.edit.height())
|
|
// make editor resizable
|
|
if (!ui.area.resize.handle.length) {
|
|
ui.area.edit.resizable({
|
|
handles: 'e',
|
|
maxWidth: $(window).width() * 0.7,
|
|
minWidth: $(window).width() * 0.2,
|
|
create: function (e, ui) {
|
|
$(this).parent().on('resize', function (e) {
|
|
e.stopPropagation()
|
|
})
|
|
},
|
|
start: function (e) {
|
|
editor.setOption('viewportMargin', Infinity)
|
|
},
|
|
resize: function (e) {
|
|
ui.area.resize.syncToggle.stop(true, true).show()
|
|
checkTocStyle()
|
|
},
|
|
stop: function (e) {
|
|
lastEditorWidth = ui.area.edit.width()
|
|
// workaround that scroll event bindings
|
|
window.preventSyncScrollToView = 2
|
|
window.preventSyncScrollToEdit = true
|
|
editor.setOption('viewportMargin', viewportMargin)
|
|
if (editorHasFocus()) {
|
|
windowResizeInner(function () {
|
|
ui.area.codemirrorScroll.scroll()
|
|
})
|
|
} else {
|
|
windowResizeInner(function () {
|
|
ui.area.view.scroll()
|
|
})
|
|
}
|
|
checkEditorScrollbar()
|
|
}
|
|
})
|
|
ui.area.resize.handle = $('.ui-resizable-handle')
|
|
}
|
|
if (!ui.area.resize.syncToggle.length) {
|
|
ui.area.resize.syncToggle = $('<button class="btn btn-lg btn-default ui-sync-toggle" title="Toggle sync scrolling"><i class="fa fa-link fa-fw"></i></button>')
|
|
ui.area.resize.syncToggle.hover(function () {
|
|
previousFocusOnEditor = editorHasFocus()
|
|
}, function () {
|
|
previousFocusOnEditor = null
|
|
})
|
|
ui.area.resize.syncToggle.click(function () {
|
|
appState.syncscroll = !appState.syncscroll
|
|
checkSyncToggle()
|
|
})
|
|
ui.area.resize.handle.append(ui.area.resize.syncToggle)
|
|
ui.area.resize.syncToggle.hide()
|
|
ui.area.resize.handle.hover(function () {
|
|
ui.area.resize.syncToggle.stop(true, true).delay(200).fadeIn(100)
|
|
}, function () {
|
|
ui.area.resize.syncToggle.stop(true, true).delay(300).fadeOut(300)
|
|
})
|
|
}
|
|
}
|
|
|
|
function checkSyncToggle () {
|
|
if (appState.syncscroll) {
|
|
if (previousFocusOnEditor) {
|
|
window.preventSyncScrollToView = false
|
|
syncScrollToView()
|
|
} else {
|
|
window.preventSyncScrollToEdit = false
|
|
syncScrollToEdit()
|
|
}
|
|
ui.area.resize.syncToggle.find('i').removeClass('fa-unlink').addClass('fa-link')
|
|
} else {
|
|
ui.area.resize.syncToggle.find('i').removeClass('fa-link').addClass('fa-unlink')
|
|
}
|
|
}
|
|
|
|
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 (appState.currentMode === modeType.both) {
|
|
ui.toc.toc.addClass('scrollspy-view')
|
|
ui.toc.affix.addClass('scrollspy-view')
|
|
} else if (appState.currentMode !== modeType.both && !newbool) {
|
|
ui.toc.toc.addClass('scrollspy-body')
|
|
ui.toc.affix.addClass('scrollspy-body')
|
|
} else {
|
|
ui.toc.toc.addClass('scrollspy-view')
|
|
ui.toc.affix.addClass('scrollspy-body')
|
|
}
|
|
if (newbool !== enoughForAffixToc) {
|
|
enoughForAffixToc = newbool
|
|
generateScrollspy()
|
|
}
|
|
}
|
|
|
|
function showStatus (type, num) {
|
|
currentStatus = type
|
|
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 (appState.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 = appState.currentMode
|
|
appState.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 (appState.currentMode) {
|
|
case modeType.edit:
|
|
ui.area.edit.show()
|
|
ui.area.view.hide()
|
|
if (!editShown) {
|
|
editor.refresh()
|
|
editShown = true
|
|
}
|
|
break
|
|
case modeType.view:
|
|
ui.area.edit.hide()
|
|
ui.area.view.show()
|
|
break
|
|
case modeType.both:
|
|
ui.area.codemirror.addClass(scrollClass)
|
|
ui.area.edit.addClass(responsiveClass).show()
|
|
ui.area.view.addClass(scrollClass)
|
|
ui.area.view.show()
|
|
break
|
|
}
|
|
// save mode to url
|
|
if (history.replaceState && window.loaded) history.replaceState(null, '', serverurl + '/' + noteid + '?' + appState.currentMode.name)
|
|
if (appState.currentMode === modeType.view) {
|
|
editor.getInputField().blur()
|
|
}
|
|
if (appState.currentMode === modeType.edit || appState.currentMode === modeType.both) {
|
|
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 (appState.currentMode !== modeType.edit) {
|
|
$(document.body).css('background-color', 'white')
|
|
updateView()
|
|
} else {
|
|
$(document.body).css('background-color', ui.area.codemirror.css('background-color'))
|
|
}
|
|
// check resizable editor style
|
|
if (appState.currentMode === modeType.both) {
|
|
if (lastEditorWidth > 0) {
|
|
ui.area.edit.css('width', lastEditorWidth + 'px')
|
|
} else {
|
|
ui.area.edit.css('width', '')
|
|
}
|
|
ui.area.resize.handle.show()
|
|
} else {
|
|
ui.area.edit.css('width', '')
|
|
ui.area.resize.handle.hide()
|
|
}
|
|
|
|
windowResizeInner()
|
|
|
|
restoreInfo()
|
|
|
|
if (lastMode === modeType.view && appState.currentMode === modeType.both) {
|
|
window.preventSyncScrollToView = 2
|
|
syncScrollToEdit(null, true)
|
|
}
|
|
|
|
if (lastMode === modeType.edit && appState.currentMode === modeType.both) {
|
|
window.preventSyncScrollToEdit = 2
|
|
syncScrollToView(null, true)
|
|
}
|
|
|
|
if (lastMode === modeType.both && appState.currentMode !== modeType.both) {
|
|
window.preventSyncScrollToView = false
|
|
window.preventSyncScrollToEdit = false
|
|
}
|
|
|
|
if (lastMode !== modeType.edit && appState.currentMode === modeType.edit) {
|
|
editor.refresh()
|
|
}
|
|
|
|
$(document.body).scrollspy('refresh')
|
|
ui.area.view.scrollspy('refresh')
|
|
|
|
ui.toolbar.both.removeClass('active')
|
|
ui.toolbar.edit.removeClass('active')
|
|
ui.toolbar.view.removeClass('active')
|
|
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 (appState.currentMode === modeType.both) {
|
|
if (editor.getScrollInfo().top !== 0) { editor.scrollTo(0, 0) } else {
|
|
ui.area.view.animate({
|
|
scrollTop: 0
|
|
}, 100, 'linear')
|
|
}
|
|
} else {
|
|
$('body, html').stop(true, true).animate({
|
|
scrollTop: 0
|
|
}, 100, 'linear')
|
|
}
|
|
}
|
|
|
|
function scrollToBottom () {
|
|
if (appState.currentMode === modeType.both) {
|
|
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)
|
|
})
|
|
|
|
ui.toolbar.night.click(function () {
|
|
toggleNightMode()
|
|
})
|
|
// permission
|
|
// freely
|
|
ui.infobar.permission.freely.click(function () {
|
|
emitPermission('freely')
|
|
})
|
|
// editable
|
|
ui.infobar.permission.editable.click(function () {
|
|
emitPermission('editable')
|
|
})
|
|
// locked
|
|
ui.infobar.permission.locked.click(function () {
|
|
emitPermission('locked')
|
|
})
|
|
// private
|
|
ui.infobar.permission.private.click(function () {
|
|
emitPermission('private')
|
|
})
|
|
// limited
|
|
ui.infobar.permission.limited.click(function () {
|
|
emitPermission('limited')
|
|
})
|
|
// protected
|
|
ui.infobar.permission.protected.click(function () {
|
|
emitPermission('protected')
|
|
})
|
|
// delete note
|
|
ui.infobar.delete.click(function () {
|
|
$('.delete-modal').modal('show')
|
|
})
|
|
$('.ui-delete-modal-confirm').click(function () {
|
|
socket.emit('delete')
|
|
})
|
|
|
|
function toggleNightMode () {
|
|
var $body = $('body')
|
|
var isActive = ui.toolbar.night.hasClass('active')
|
|
if (isActive) {
|
|
$body.removeClass('night')
|
|
appState.nightMode = false
|
|
} else {
|
|
$body.addClass('night')
|
|
appState.nightMode = true
|
|
}
|
|
if (store.enabled) {
|
|
store.set('nightMode', !isActive)
|
|
} else {
|
|
Cookies.set('nightMode', !isActive, {
|
|
expires: 365
|
|
})
|
|
}
|
|
}
|
|
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/' : '',
|
|
query: {
|
|
noteId: noteid
|
|
},
|
|
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) { appState.currentMode = modeType.edit } else { appState.currentMode = modeType.both }
|
|
}
|
|
// parse mode from url
|
|
if (window.location.search.length > 0) {
|
|
var urlMode = modeType[window.location.search.substr(1)]
|
|
if (urlMode) appState.currentMode = urlMode
|
|
}
|
|
changeMode(appState.currentMode)
|
|
if (nocontent && !visibleXS) {
|
|
editor.focus()
|
|
editor.refresh()
|
|
}
|
|
updateViewInner() // bring up view rendering earlier
|
|
updateHistory() // update history whether have content or not
|
|
window.loaded = true
|
|
emitUserStatus() // send first user status
|
|
updateOnlineStatus() // update first online status
|
|
setTimeout(function () {
|
|
// work around editor not refresh or doc not fully loaded
|
|
windowResizeInner()
|
|
// work around might not scroll to hash
|
|
scrollToHash()
|
|
}, 1)
|
|
}
|
|
if (editor.getOption('readOnly')) { editor.setOption('readOnly', false) }
|
|
})
|
|
|
|
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 (appState.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 ($('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 (appState.currentMode) {
|
|
case modeType.edit:
|
|
if (scrollbarStyle === 'native') {
|
|
lastInfo.edit.scroll.left = left
|
|
lastInfo.edit.scroll.top = top
|
|
} else {
|
|
lastInfo.edit.scroll = editor.getScrollInfo()
|
|
}
|
|
break
|
|
case modeType.view:
|
|
lastInfo.view.scroll.left = left
|
|
lastInfo.view.scroll.top = top
|
|
break
|
|
case modeType.both:
|
|
lastInfo.edit.scroll = editor.getScrollInfo()
|
|
lastInfo.view.scroll.left = ui.area.view.scrollLeft()
|
|
lastInfo.view.scroll.top = ui.area.view.scrollTop()
|
|
break
|
|
}
|
|
lastInfo.edit.cursor = editor.getCursor()
|
|
lastInfo.edit.selections = editor.listSelections()
|
|
lastInfo.needRestore = true
|
|
}
|
|
|
|
function restoreInfo () {
|
|
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 (appState.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 (appState.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')
|
|
appState.syncscroll = false
|
|
checkSyncToggle()
|
|
} else {
|
|
if (lastMeta.type && lastMeta.type === 'slide') {
|
|
refreshView()
|
|
ui.area.markdown.removeClass('slides')
|
|
appState.syncscroll = true
|
|
checkSyncToggle()
|
|
}
|
|
// only render again when meta changed
|
|
if (JSON.stringify(md.meta) !== JSON.stringify(lastMeta)) {
|
|
parseMeta(md, ui.area.codemirror, ui.area.markdown, $('#ui-toc'), $('#ui-toc-affix'))
|
|
rendered = md.render(value)
|
|
}
|
|
// prevent XSS
|
|
rendered = preventXSS(rendered)
|
|
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)
|
|
}
|
|
})
|