HackMD/public/js/index.js

3979 lines
132 KiB
JavaScript

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