summaryrefslogtreecommitdiff
path: root/public/js/index.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--public/js/index.js985
1 files changed, 890 insertions, 95 deletions
diff --git a/public/js/index.js b/public/js/index.js
index 331251c9..24b38f75 100644
--- a/public/js/index.js
+++ b/public/js/index.js
@@ -1,11 +1,110 @@
//constant vars
//settings
-var debug = true;
-var version = '0.2.8';
+var debug = false;
+var version = '0.2.9';
+
+var defaultTextHeight = 18;
+var viewportMargin = 20;
+var defaultExtraKeys = {
+ "Enter": "newlineAndIndentContinueMarkdownList"
+};
+
+var idleTime = 300000; //5 mins
var doneTypingDelay = 400;
var finishChangeDelay = 400;
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 supportGenerals = [
+ {
+ command: function () {
+ return moment().format('llll');
+ },
+ search: 'time'
+ }
+];
var modeType = {
edit: {},
view: {},
@@ -18,7 +117,7 @@ var statusType = {
fa: "fa-wifi"
},
online: {
- msg: "ONLINE: ",
+ msg: "ONLINE",
label: "label-primary",
fa: "fa-users"
},
@@ -63,6 +162,8 @@ var lastInfo = {
},
history: null
};
+var personalInfo = {};
+var onlineUsers = [];
//editor settings
var textit = document.getElementById("textit");
@@ -70,15 +171,16 @@ if (!textit) throw new Error("There was no textit area!");
var editor = CodeMirror.fromTextArea(textit, {
mode: 'gfm',
keyMap: "sublime",
- viewportMargin: 20,
+ viewportMargin: viewportMargin,
styleActiveLine: true,
lineNumbers: true,
lineWrapping: true,
showCursorWhenSelecting: true,
+ indentUnit: 4,
+ indentWithTabs: true,
+ continueComments: "Enter",
theme: "monokai",
- autofocus: true,
inputStyle: "textarea",
- scrollbarStyle: "overlay",
matchBrackets: true,
autoCloseBrackets: true,
matchTags: {
@@ -87,12 +189,11 @@ var editor = CodeMirror.fromTextArea(textit, {
autoCloseTags: true,
foldGutter: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
- extraKeys: {
- "Enter": "newlineAndIndentContinueMarkdownList"
- },
+ extraKeys: defaultExtraKeys,
readOnly: true
});
inlineAttachment.editors.codemirror4.attach(editor);
+defaultTextHeight = parseInt($(".CodeMirror").css('line-height'));
//ui vars
var ui = {
@@ -146,9 +247,59 @@ var opts = {
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();
+}
+
+loginStateChangeEvent = function () {
+ location.reload(true);
+}
+
+//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) {
@@ -174,26 +325,52 @@ $(window).resize(function () {
windowResize();
}, windowResizeDelay);
});
+//when page unload
+$(window).unload(function () {
+ emitRefresh();
+});
+
function windowResize() {
checkResponsive();
- clearMap();
- syncScrollToView();
+ checkEditorStyle();
+ 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 (editor.hasFocus())
+ if (editorHasFocus())
changeMode(modeType.edit);
else
changeMode(modeType.view);
- if (visibleXS)
- $('.CodeMirror').css('height', 'auto');
- else
- $('.CodeMirror').css('height', '');
+
+ 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 showStatus(type, num) {
@@ -217,8 +394,8 @@ function showStatus(type, num) {
case statusType.online:
label.addClass(statusType.online.label);
fa.addClass(statusType.online.fa);
- shortMsg = " " + num;
- msg = statusType.online.msg + num;
+ shortMsg = num;
+ msg = num + " " + statusType.online.msg;
break;
case statusType.offline:
label.addClass(statusType.offline.label);
@@ -255,6 +432,7 @@ 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);
@@ -282,7 +460,7 @@ function changeMode(type) {
break;
}
if (currentMode != modeType.view && visibleLG) {
- editor.focus();
+ //editor.focus();
editor.refresh();
} else {
editor.getInputField().blur();
@@ -291,6 +469,8 @@ function changeMode(type) {
updateView();
restoreInfo();
+ windowResize();
+
ui.toolbar.both.removeClass("active");
ui.toolbar.edit.removeClass("active");
ui.toolbar.view.removeClass("active");
@@ -436,6 +616,17 @@ ui.toolbar.both.click(function () {
//socket.io actions
var socket = io.connect();
+//overwrite original event for checking login state
+var on = socket.on;
+socket.on = function () {
+ if (!checkLoginStateChanged())
+ on.apply(socket, arguments);
+};
+var emit = socket.emit;
+socket.emit = function () {
+ if (!checkLoginStateChanged())
+ emit.apply(socket, arguments);
+};
socket.on('info', function (data) {
console.error(data);
location.href = "./404.html";
@@ -449,7 +640,14 @@ socket.on('disconnect', function (data) {
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');
});
@@ -461,7 +659,7 @@ socket.on('refresh', function (data) {
saveInfo();
var body = data.body;
- body = LZString.decompressFromBase64(body);
+ body = LZString.decompressFromUTF16(body);
if (body)
editor.setValue(body);
else
@@ -473,8 +671,11 @@ socket.on('refresh', function (data) {
ui.content.fadeIn();
changeMode();
loaded = true;
+ emitUserStatus(); //send first user status
+ updateOnlineStatus(); //update first online status
} else {
- if (LZString.compressToBase64(editor.getValue()) !== data.body)
+ //if current doc is equal to the doc before disconnect
+ if (LZString.compressToUTF16(editor.getValue()) !== data.body)
editor.clearHistory();
else {
if (lastInfo.history)
@@ -491,7 +692,7 @@ socket.on('refresh', function (data) {
restoreInfo();
});
socket.on('change', function (data) {
- data = LZString.decompressFromBase64(data);
+ data = LZString.decompressFromUTF16(data);
data = JSON.parse(data);
editor.replaceRange(data.text, data.from, data.to, "ignoreHistory");
isDirty = true;
@@ -499,9 +700,12 @@ socket.on('change', function (data) {
finishChangeTimer = setTimeout(finishChange, finishChangeDelay);
});
socket.on('online users', function (data) {
+ data = LZString.decompressFromUTF16(data);
+ data = JSON.parse(data);
if (debug)
console.debug(data);
- showStatus(statusType.online, data.count);
+ onlineUsers = data.users;
+ updateOnlineStatus();
$('.other-cursors').children().each(function (key, value) {
var found = false;
for (var i = 0; i < data.users.length; i++) {
@@ -510,85 +714,409 @@ socket.on('online users', function (data) {
found = true;
}
if (!found)
- $(this).remove();
+ $(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.id, user.color, user.cursor);
+ 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.fadeIn();
- } else {
- if (data.id != socket.id)
- buildCursor(data.id, data.color, data.cursor);
+ 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.id, data.color, data.cursor);
+ 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.fadeOut();
+ cursor.stop(true).fadeOut();
}
});
-function emitUserStatus() {
- checkIfAuth(
- function (data) {
- socket.emit('user status', {
- login: true
- });
- },
- function () {
- socket.emit('user status', {
- login: false
- });
+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="fa fa-square 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) 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.toLowerCase() < userb.name.toLowerCase()) {
+ return -1;
+ } else if (usera.name.toLowerCase() > userb.name.toLowerCase()) {
+ return 1;
+ } else {
+ if (usera.color.toLowerCase() < userb.color.toLowerCase())
+ return -1;
+ else if (usera.color.toLowerCase() > userb.color.toLowerCase())
+ return 1;
+ else
+ return 0;
+ }
+ }
+ }
+ }
+ }
+ });
}
-function buildCursor(id, color, pos) {
- if (!pos) return;
+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');
+ usericon.css('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;
+ }
+ 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) {
+ var curosrtagMargin = 60;
+ var viewport = editor.getViewport();
+ var viewportHeight = (viewport.to - viewport.from) * editor.defaultTextHeight();
+ var editorWidth = ui.area.codemirror.width();
+ var editorHeight = ui.area.codemirror.height();
+ var width = ele.width();
+ var height = ele.height();
+ var left = coord.left;
+ var top = coord.top;
+ var offsetLeft = -3;
+ var offsetTop = defaultTextHeight;
+ if (width > 0 && height > 0) {
+ if (left + width + offsetLeft > editorWidth - curosrtagMargin) {
+ offsetLeft = -(width + 4);
+ }
+ if (top + height + offsetTop > Math.max(viewportHeight, editorHeight) + curosrtagMargin && top - height > curosrtagMargin) {
+ offsetTop = -(height);
+ }
+ }
+ ele[0].style.left = offsetLeft + 'px';
+ ele[0].style.top = offsetTop + 'px';
+}
+
+function buildCursor(user) {
+ 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 ($('#' + id).length <= 0) {
- var cursor = $('<div id="' + id + '" class="other-cursor">&nbsp;</div>');
- //console.debug(pos);
- cursor.attr('data-line', pos.line);
- cursor.attr('data-ch', pos.ch);
- var coord = editor.charCoords(pos, 'windows');
+ if ($('#' + user.id).length <= 0) {
+ var cursor = $('<div id="' + 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', 'state');
+ 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';
- cursor[0].style.height = '18px';
- cursor[0].style.borderLeft = '2px solid ' + color;
$('.other-cursors').append(cursor);
- cursor.hide().fadeIn();
+
+ if (!user.idle)
+ cursor.stop(true).fadeIn();
+
+ checkCursorTag(coord, cursortag);
} else {
- var cursor = $('#' + id);
- cursor.attr('data-line', pos.line);
- cursor.attr('data-ch', pos.ch);
- var coord = editor.charCoords(pos, 'windows');
- cursor.stop(true).css('opacity', 1).animate({
- "left": coord.left,
- "top": coord.top
- }, cursorAnimatePeriod);
- //cursor[0].style.left = coord.left + 'px';
- //cursor[0].style.top = coord.top + 'px';
- cursor[0].style.height = '18px';
- cursor[0].style.borderLeft = '2px solid ' + color;
+ var cursor = $('#' + user.id);
+ 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);
}
}
@@ -601,13 +1129,19 @@ editor.on('change', function (i, op) {
if (debug)
console.debug(op);
if (op.origin != 'setValue' && op.origin != 'ignoreHistory') {
- socket.emit('change', LZString.compressToBase64(JSON.stringify(op)));
+ socket.emit('change', LZString.compressToUTF16(JSON.stringify(op)));
}
isDirty = true;
clearTimeout(doneTypingTimer);
doneTypingTimer = setTimeout(doneTyping, doneTypingDelay);
});
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());
});
var cursorActivityTimer = null;
@@ -617,20 +1151,38 @@ editor.on('cursorActivity', function (cm) {
});
function cursorActivity() {
- socket.emit('cursor activity', editor.getCursor());
+ 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 = $(document.body).scrollLeft();
var top = $(document.body).scrollTop();
switch (currentMode) {
case modeType.edit:
- //lastInfo.edit.scroll.left = left;
- //lastInfo.edit.scroll.top = top;
- lastInfo.edit.scroll = editor.getScrollInfo();
+ 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;
@@ -647,6 +1199,7 @@ function saveInfo() {
}
function restoreInfo() {
+ var scrollbarStyle = editor.getOption('scrollbarStyle');
if (lastInfo.needRestore) {
var line = lastInfo.edit.cursor.line;
var ch = lastInfo.edit.cursor.ch;
@@ -654,12 +1207,15 @@ function restoreInfo() {
switch (currentMode) {
case modeType.edit:
- //$(document.body).scrollLeft(lastInfo.edit.scroll.left);
- //$(document.body).scrollTop(lastInfo.edit.scroll.top);
- var left = lastInfo.edit.scroll.left;
- var top = lastInfo.edit.scroll.top;
- editor.scrollIntoView();
- editor.scrollTo(left, top);
+ if (scrollbarStyle == 'native') {
+ $(document.body).scrollLeft(lastInfo.edit.scroll.left);
+ $(document.body).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:
$(document.body).scrollLeft(lastInfo.view.scroll.left);
@@ -685,15 +1241,19 @@ var finishChangeTimer = null;
var input = editor.getInputField();
//user is "finished typing," do something
function doneTyping() {
+ emitRefresh();
updateView();
- var value = editor.getValue();
- socket.emit('refresh', LZString.compressToBase64(value));
}
function finishChange() {
updateView();
}
+function emitRefresh() {
+ var value = editor.getValue();
+ socket.emit('refresh', LZString.compressToUTF16(value));
+}
+
var lastResult = null;
function updateView() {
@@ -703,12 +1263,22 @@ function updateView() {
//ui.area.markdown.html(result);
//finishView(ui.area.markdown);
partialUpdate(result, lastResult, ui.area.markdown.children().toArray());
- lastResult = $(result).clone(true);
+ if (result && lastResult && result.length != lastResult.length)
+ updateDataAttrs(result, ui.area.markdown.children().toArray());
+ lastResult = $(result).clone();
finishView(ui.area.view);
writeHistory(ui.area.markdown);
isDirty = false;
- emitUserStatus();
clearMap();
+ buildMap();
+}
+
+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) {
@@ -733,8 +1303,8 @@ function partialUpdate(src, tar, des) {
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');
+ //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) {
@@ -746,8 +1316,8 @@ function partialUpdate(src, tar, des) {
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');
+ //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) {
@@ -759,8 +1329,8 @@ function partialUpdate(src, tar, des) {
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');
+ //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) {
@@ -772,8 +1342,8 @@ function partialUpdate(src, tar, des) {
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');
+ //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) {
@@ -805,12 +1375,12 @@ function partialUpdate(src, tar, des) {
var repeatDiff = Math.abs(srcEnd - tarEnd) - 1;
//push new elements
var newElements = [];
- if(srcEnd >= start) {
+ if (srcEnd >= start) {
for (var j = start; j <= srcEnd; j++) {
if (!src[j]) continue;
newElements.push(src[j].outerHTML);
}
- } else if(repeatAdd) {
+ } else if (repeatAdd) {
for (var j = srcEnd - repeatDiff; j <= srcEnd; j++) {
if (!des[j]) continue;
newElements.push(des[j].outerHTML);
@@ -818,12 +1388,12 @@ function partialUpdate(src, tar, des) {
}
//push remove elements
var removeElements = [];
- if(tarEnd >= start) {
+ if (tarEnd >= start) {
for (var j = start; j <= tarEnd; j++) {
if (!des[j]) continue;
removeElements.push(des[j]);
}
- } else if(!repeatAdd) {
+ } else if (!repeatAdd) {
for (var j = start; j <= start + repeatDiff; j++) {
if (!des[j]) continue;
removeElements.push(des[j]);
@@ -853,7 +1423,7 @@ function partialUpdate(src, tar, des) {
function cloneAndRemoveDataAttr(el) {
if (!el) return;
- var rawEl = $(el).clone(true)[0];
+ var rawEl = $(el).clone()[0];
rawEl.removeAttribute('data-startline');
rawEl.removeAttribute('data-endline');
return rawEl;
@@ -862,4 +1432,229 @@ function cloneAndRemoveDataAttr(el) {
function copyAttribute(src, des, attr) {
if (src && src.getAttribute(attr) && des)
des.setAttribute(attr, src.getAttribute(attr));
-} \ No newline at end of file
+}
+
+if ($('.cursor-menu').length <= 0) {
+ $("<div class='cursor-menu'>").insertAfter('.CodeMirror-cursors');
+}
+
+var upSideDown = false;
+var menuMargin = 60;
+
+function checkCursorMenu() {
+ var dropdown = $('.cursor-menu .dropdown-menu');
+ var cursor = editor.getCursor();
+ var scrollInfo = editor.getScrollInfo();
+ if (!dropdown.hasClass('other-cursor'))
+ dropdown.addClass('other-cursor');
+ dropdown.attr('data-line', cursor.line);
+ dropdown.attr('data-ch', cursor.ch);
+ var coord = editor.charCoords({
+ line: cursor.line,
+ ch: cursor.ch
+ }, 'windows');
+ var viewport = editor.getViewport();
+ var viewportHeight = (viewport.to - viewport.from) * editor.defaultTextHeight();
+ var editorWidth = ui.area.codemirror.width();
+ var editorHeight = ui.area.codemirror.height();
+ var width = dropdown.width();
+ var height = dropdown.height();
+ var left = coord.left;
+ var top = coord.top;
+ var offsetLeft = 0;
+ var offsetTop = defaultTextHeight;
+ if (left + width + offsetLeft > editorWidth - menuMargin)
+ offsetLeft = -(left + width - editorWidth + menuMargin);
+ if (top + height + offsetTop > Math.max(viewportHeight, editorHeight) + menuMargin && top - height > menuMargin) {
+ offsetTop = -(height + defaultTextHeight);
+ upSideDown = true;
+ } else {
+ upSideDown = false;
+ }
+ dropdown.attr('data-offset-left', offsetLeft);
+ dropdown.attr('data-offset-top', offsetTop);
+ dropdown[0].style.left = left + offsetLeft + 'px';
+ dropdown[0].style.top = top + offsetTop + 'px';
+}
+
+var isInCode = false;
+
+function check(text) {
+ var cursor = editor.getCursor();
+ text = [];
+ for (var i = 0; i < cursor.line; i++)
+ text.push(editor.getLine(i));
+ text = text.join('\n') + '\n' + editor.getLine(cursor.line).slice(0, cursor.ch);
+ //console.log(text);
+ var match;
+ match = text.match(/`{3,}/g);
+ if (match && match.length % 2) {
+ isInCode = true;
+ } else {
+ match = text.match(/`/g);
+ if (match && match.length % 2) {
+ isInCode = true;
+ } else {
+ isInCode = false;
+ }
+ }
+}
+
+$(editor.getInputField())
+ .textcomplete([
+ { // emoji strategy
+ match: /(?:^|\n|)\B:([\-+\w]*)$/,
+ search: function (term, callback) {
+ callback($.map(emojify.emojiNames, function (emoji) {
+ return emoji.indexOf(term) === 0 ? emoji : null;
+ }));
+ checkCursorMenu();
+ },
+ template: function (value) {
+ return '<img class="emoji" src="/vendor/emojify/images/' + value + '.png"></img> ' + value;
+ },
+ replace: function (value) {
+ return ':' + value + ':';
+ },
+ index: 1,
+ context: function (text) {
+ check(text);
+ return !isInCode;
+ }
+ },
+ { // Code block language strategy
+ langs: supportCodeModes,
+ match: /(^|\n)```(\w*)$/,
+ search: function (term, callback) {
+ callback($.map(this.langs, function (lang) {
+ return lang.indexOf(term) === 0 ? lang : null;
+ }));
+ checkCursorMenu();
+ },
+ replace: function (lang) {
+ return '$1```' + lang + '=\n\n```';
+ },
+ done: function () {
+ editor.doc.cm.moveV(-1, "line");
+ },
+ context: function () {
+ return isInCode;
+ }
+ },
+ { //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;
+ }));
+ checkCursorMenu();
+ },
+ replace: function (value) {
+ return '$1' + value;
+ },
+ context: function (text) {
+ return !isInCode;
+ }
+ },
+ { //referral
+ match: /(^|\n|\s)(\!|\!|\[\])(\w*)$/,
+ search: function (term, callback) {
+ callback($.map(supportReferrals, function (referral) {
+ return referral.search.indexOf(term) === 0 ? referral.text : null;
+ }));
+ checkCursorMenu();
+ },
+ 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;
+ }));
+ checkCursorMenu();
+ },
+ replace: function (value) {
+ return '$1' + value;
+ },
+ context: function (text) {
+ return !isInCode;
+ }
+ },
+ { //blockquote personal info & general info
+ match: /(^|\n|\s|\>.*)\[(\w*)=$/,
+ search: function (term, callback) {
+ var list = typeof personalInfo[term] != 'undefined' ? [personalInfo[term]] : [];
+ $.map(supportGenerals, function (general) {
+ if (general.search.indexOf(term) === 0)
+ list.push(general.command());
+ });
+ callback(list);
+ checkCursorMenu();
+ },
+ replace: function (value) {
+ return '$1[$2=' + value;
+ },
+ context: function (text) {
+ return !isInCode;
+ }
+ },
+ { //blockquote quick start tag
+ match: /(^.*(?!>)\n|)(\>\s{0,1})$/,
+ search: function (term, callback) {
+ var self = '[name=' + personalInfo.name + '] [time=' + moment().format('llll') + '] [color=' + personalInfo.color + ']';
+ callback([self]);
+ checkCursorMenu();
+ },
+ template: function (value) {
+ return '[Your name, time, color tags]';
+ },
+ replace: function (value) {
+ return '$1$2' + value;
+ },
+ context: function (text) {
+ return !isInCode;
+ }
+ }
+], {
+ appendTo: $('.cursor-menu')
+ })
+ .on({
+ 'textComplete:select': function (e, value, strategy) {
+ //NA
+ },
+ 'textComplete:show': function (e) {
+ checkCursorMenu();
+ $(this).data('autocompleting', true);
+ editor.setOption("extraKeys", {
+ "Up": function () {
+ return CodeMirror.PASS;
+ },
+ "Right": function () {
+ editor.doc.cm.execCommand("goCharRight");
+ },
+ "Down": function () {
+ return CodeMirror.PASS;
+ },
+ "Left": function () {
+ editor.doc.cm.execCommand("goCharLeft");
+ },
+ "Enter": function () {
+ return CodeMirror.PASS;
+ },
+ "Backspace": function () {
+ editor.doc.cm.execCommand("delCharBefore");
+ checkCursorMenu();
+ }
+ });
+ },
+ 'textComplete:hide': function (e) {
+ $(this).data('autocompleting', false);
+ editor.setOption("extraKeys", defaultExtraKeys);
+ }
+ }); \ No newline at end of file