//constant vars //settings var debug = false; var version = '0.3.2'; var defaultTextHeight = 18; var viewportMargin = 20; var defaultExtraKeys = { "Cmd-S": function () { return CodeMirror.PASS }, "Ctrl-S": function () { return CodeMirror.PASS }, "Enter": "newlineAndIndentContinueMarkdownList" }; var idleTime = 300000; //5 mins var finishChangeDelay = 200; var cursorActivityDelay = 50; var cursorAnimatePeriod = 100; var supportCodeModes = ['javascript', 'htmlmixed', 'htmlembedded', 'css', 'xml', 'clike', 'clojure', 'ruby', 'python', 'shell', 'php', 'sql', 'coffeescript', 'yaml', 'jade', 'lua', 'cmake', 'nginx', 'perl', 'sass', 'r', 'dockerfile']; 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]: url "title"', search: '[]:' }, { text: '[^footnote link]', search: '[^]' }, { text: '[^footnote reference]: url "title"', search: '[^]:' }, { text: '^[inline footnote]', search: '^[]' }, { text: '[link text][reference]', search: '[][]' }, { text: '[link text](url "title")', search: '[]()' }, { text: '![image text][reference]', search: '![][]' }, { text: '![image text](url "title")', search: '![]()' } ]; var supportExternals = [ { text: '{%youtube youtubeid %}', search: 'youtube' }, { text: '{%vimeo vimeoid %}', search: 'vimeo' }, { text: '{%gist gistid %}', search: 'gist' } ]; var supportBlockquoteTags = [ { 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({ luminosity: 'light' }); return '[color=' + color + ']'; } } ]; var modeType = { edit: {}, view: {}, 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.both; //global vars var loaded = false; var needRefresh = false; var isDirty = false; var editShown = false; var visibleXS = false; var visibleSM = false; var visibleMD = false; var visibleLG = false; var isTouchDevice = 'ontouchstart' in document.documentElement; var currentMode = defaultMode; var currentStatus = statusType.offline; var lastInfo = { needRestore: false, cursor: null, scroll: null, edit: { scroll: { left: null, top: null }, cursor: { line: null, ch: null } }, view: { scroll: { left: null, top: null } }, history: null }; var personalInfo = {}; var onlineUsers = []; //editor settings var textit = document.getElementById("textit"); if (!textit) throw new Error("There was no textit area!"); var editor = CodeMirror.fromTextArea(textit, { mode: 'gfm', keyMap: "sublime", viewportMargin: viewportMargin, styleActiveLine: true, lineNumbers: true, lineWrapping: true, showCursorWhenSelecting: true, indentUnit: 4, indentWithTabs: true, continueComments: "Enter", theme: "monokai", inputStyle: "textarea", matchBrackets: true, autoCloseBrackets: true, matchTags: { bothTags: true }, autoCloseTags: true, foldGutter: true, gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"], extraKeys: defaultExtraKeys, readOnly: true }); var inlineAttach = inlineAttachment.editors.codemirror4.attach(editor); defaultTextHeight = parseInt($(".CodeMirror").css('line-height')); //ui vars var ui = { spinner: $(".ui-spinner"), content: $(".ui-content"), toolbar: { shortStatus: $(".ui-short-status"), status: $(".ui-status"), new: $(".ui-new"), publish: $(".ui-publish"), download: { markdown: $(".ui-download-markdown") }, save: { dropbox: $(".ui-save-dropbox") }, import: { dropbox: $(".ui-import-dropbox"), clipboard: $(".ui-import-clipboard") }, mode: $(".ui-mode"), edit: $(".ui-edit"), view: $(".ui-view"), both: $(".ui-both"), uploadImage: $(".ui-upload-image") }, infobar: { lastchange: $(".ui-lastchange"), permission: { permission: $(".ui-permission"), label: $(".ui-permission-label"), freely: $(".ui-permission-freely"), editable: $(".ui-permission-editable"), locked: $(".ui-permission-locked") } }, 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"), markdown: $(".ui-view-area .markdown-body") } }; //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: idleStateChange, onAwayBack: idleStateChange, awayTimeout: idleTime }); ui.area.codemirror.on('touchstart', function () { idle.onActive(); }); function idleStateChange() { emitUserStatus(); updateOnlineStatus(); } function setNeedRefresh() { $('#refreshModal').modal('show'); needRefresh = true; editor.setOption('readOnly', true); socket.disconnect(); showStatus(statusType.offline); } loginStateChangeEvent = 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) { editor.focus(); wasFocus = false; } } }); //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(); changeMode(currentMode); /* 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' }); }); //when page resize var windowResizeDelay = 200; var windowResizeTimer = null; $(window).resize(function () { clearTimeout(windowResizeTimer); windowResizeTimer = setTimeout(function () { windowResize(); }, windowResizeDelay); }); //when page unload $(window).unload(function () { writeHistory(ui.area.markdown); }); $(window).error(function () { setNeedRefresh(); }); //when page hash change window.onhashchange = locationHashChanged; function locationHashChanged(e) { e.stopPropagation(); e.preventDefault(); if (currentMode != modeType.both) { return; } var hashtarget = $("[id$='" + location.hash.substr(1) + "']"); if (hashtarget.length > 0) { var linenumber = hashtarget.attr('data-startline'); if (linenumber) { editor.setOption('viewportMargin', Infinity); editor.setOption('viewportMargin', viewportMargin); var t = editor.charCoords({ line: linenumber, ch: 0 }, "local").top; editor.scrollTo(null, t - defaultTextHeight * 1.2); } } } function windowResize() { checkResponsive(); checkEditorStyle(); checkTocStyle(); //refresh editor if (loaded) { editor.setOption('viewportMargin', Infinity); setTimeout(function () { clearMap(); syncScrollToView(); editor.setOption('viewportMargin', viewportMargin); }, windowResizeDelay); } } 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(); } function checkEditorStyle() { var scrollbarStyle = editor.getOption('scrollbarStyle'); if (scrollbarStyle == 'overlay' || currentMode == modeType.both) { ui.area.codemirror.css('height', ''); } else if (scrollbarStyle == 'native') { ui.area.codemirror.css('height', 'auto'); $('.CodeMirror-gutters').css('height', $('.CodeMirror-sizer').height()); } } 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'); } 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 && !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 = $(''); var fa = $(''); 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; } } function changeMode(type) { saveInfo(); if (type) currentMode = type; checkEditorStyle(); 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.addClass(responsiveClass).show(); break; } if (currentMode != modeType.view && visibleLG) { //editor.focus(); //editor.refresh(); } else { editor.getInputField().blur(); } if (currentMode == modeType.edit || currentMode == modeType.both) { ui.toolbar.uploadImage.fadeIn(); } 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')); } restoreInfo(); windowResize(); 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-toggle-on').removeClass('fa-toggle-off'); 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-toggle-off'); } else if (ui.area.view.is(":visible")) { //view ui.toolbar.view.addClass("active"); modeIcon.addClass('fa-toggle-on'); } } //button actions var url = window.location.pathname; //share ui.toolbar.publish.attr("href", url + "/publish"); //download //markdown ui.toolbar.download.markdown.click(function () { 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); }); //save to dropbox ui.toolbar.save.dropbox.click(function () { var filename = renderFilename(ui.area.markdown) + '.md'; var options = { files: [ { 'url': url + "/download", 'filename': filename } ] }; Dropbox.save(options); }); //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 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(); }); 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 { $(document.body).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 { $(document.body).animate({ scrollTop: $(document.body)[0].scrollHeight }, 100, "linear"); } } var enoughForAffixToc = true; //scrollspy function generateScrollspy() { $(document.body).scrollspy({ target: '.scrollspy-body' }); ui.area.view.scrollspy({ target: '.scrollspy-view' }); $(document.body).scrollspy('refresh'); ui.area.view.scrollspy('refresh'); if (enoughForAffixToc) { ui.toc.toc.hide(); ui.toc.affix.show(); } else { ui.toc.affix.hide(); ui.toc.toc.show(); } $(document.body).scroll(); ui.area.view.scroll(); } //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(''); } }); $('#refreshModalRefresh').click(function () { location.reload(true); }); function parseToEditor(data) { var parsed = toMarkdown(data); if (parsed) editor.replaceRange(parsed, { line: 0, ch: 0 }, { line: editor.lastLine(), ch: editor.lastLine().length }, '+input'); } function importFromUrl(url) { //console.log(url); if (url == null) return; if (!isValidURL(url)) { alert('Not valid URL :('); return; } $.ajax({ method: "GET", url: url, success: function (data) { parseToEditor(data); }, error: function () { alert('Import failed :('); }, complete: function () { ui.spinner.hide(); } }); } function isValidURL(str) { var pattern = new RegExp('^(https?:\\/\\/)?' + // protocol '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path '(\\?[;&a-z\\d%_.~+=-]*)?' + // query string '(\\#[-a-z\\d_]*)?$', 'i'); // fragment locator if (!pattern.test(str)) { return false; } else { return true; } } //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 () { updatePermission("freely"); }); //editable ui.infobar.permission.editable.click(function () { updatePermission("editable"); }); //locked ui.infobar.permission.locked.click(function () { updatePermission("locked"); }); function updatePermission(_permission) { if (_permission != permission) { socket.emit('permission', _permission); } } function checkPermission() { var label = null; var title = null; switch (permission) { case "freely": label = ' Freely'; title = "Anyone can edit"; break; case "editable": label = ' Editable'; title = "Signed people can edit"; break; case "locked": label = ' Locked'; title = "Only owner can edit"; break; } if (personalInfo.userid == owner) { label += ' '; ui.infobar.permission.label.removeClass('disabled'); } else { ui.infobar.permission.label.addClass('disabled'); } ui.infobar.permission.label.html(label).attr('title', title); } //socket.io actions var socket = io.connect(); //overwrite original event for checking login state var on = socket.on; socket.on = function () { if (!checkLoginStateChanged() && !needRefresh) on.apply(socket, arguments); }; var emit = socket.emit; socket.emit = function () { if (!checkLoginStateChanged() && !needRefresh) emit.apply(socket, arguments); }; socket.on('info', function (data) { console.error(data); location.href = "./404.html"; }); socket.on('disconnect', function (data) { showStatus(statusType.offline); if (loaded) { saveInfo(); lastInfo.history = editor.getHistory(); } if (!editor.getOption('readOnly')) editor.setOption('readOnly', true); }); socket.on('reconnect', function (data) { //sync back any change in offline emitUserStatus(true); cursorActivity(); socket.emit('online users'); }); socket.on('connect', function (data) { personalInfo['id'] = socket.id; showStatus(statusType.connected); socket.emit('version'); }); socket.on('version', function (data) { if (data != version) setNeedRefresh(); }); socket.on('check', function (data) { lastchangetime = data.updatetime; lastchangeui = ui.infobar.lastchange; updateLastChange(); }); socket.on('permission', function (data) { permission = data.permission; checkPermission(); }); var docmaxlength = null; var otk = null; var owner = null; var permission = null; socket.on('refresh', function (data) { docmaxlength = data.docmaxlength; editor.setOption("maxLength", docmaxlength); otk = data.otk; owner = data.owner; permission = data.permission; lastchangetime = data.updatetime; lastchangeui = ui.infobar.lastchange; updateLastChange(); checkPermission(); }); var EditorClient = ot.EditorClient; var SocketIOAdapter = ot.SocketIOAdapter; var CodeMirrorAdapter = ot.CodeMirrorAdapter; var cmClient = null; socket.on('doc', function (obj) { obj = LZString.decompressFromUTF16(obj); obj = JSON.parse(obj); var body = obj.str; var bodyMismatch = (editor.getValue() != body); saveInfo(); if (bodyMismatch) { if (body) editor.setValue(body); else editor.setValue(""); } if (!cmClient) { cmClient = window.cmClient = new EditorClient( obj.revision, obj.clients, new SocketIOAdapter(socket), new CodeMirrorAdapter(editor) ); } else { cmClient.revision = obj.revision; cmClient.initializeClients(obj.clients); if (bodyMismatch) { cmClient.undoManager.undoStack.length = 0; cmClient.undoManager.redoStack.length = 0; } } if (!loaded) { editor.clearHistory(); ui.spinner.hide(); ui.content.fadeIn(); changeMode(); loaded = true; emitUserStatus(); //send first user status updateOnlineStatus(); //update first online status setTimeout(function () { //work around editor not refresh editor.refresh(); //work around cursor not refresh for (var i = 0; i < onlineUsers.length; i++) { buildCursor(onlineUsers[i]); } //work around might not scroll to hash scrollToHash(); }, 1); } else { //if current doc is equal to the doc before disconnect if (bodyMismatch) editor.clearHistory(); else { if (lastInfo.history) editor.setHistory(lastInfo.history); } lastInfo.history = null; } if (bodyMismatch) { isDirty = true; updateView(); } if (editor.getOption('readOnly')) editor.setOption('readOnly', false); restoreInfo(); }); socket.on('ack', _.debounce(function () { isDirty = true; updateView(); }, finishChangeDelay)); socket.on('operation', _.debounce(function () { isDirty = true; updateView(); }, finishChangeDelay)); 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; } } if (data.id != socket.id) buildCursor(data); //force show var cursor = $('#' + 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; } } 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 = $('#' + data.id); if (cursor.length > 0) { cursor.stop(true).fadeOut(); } }); var options = { valueNames: ['id', 'name'], item: '