@@ -188,6 +188,8 @@
+
+
diff --git a/public/js/common.js b/public/js/common.js
index 37591d36..e6928e98 100644
--- a/public/js/common.js
+++ b/public/js/common.js
@@ -3,38 +3,62 @@ var domain = 'change this';
var checkAuth = false;
var profile = null;
var lastLoginState = getLoginState();
+var lastUserId = getUserId();
var loginStateChangeEvent = null;
function resetCheckAuth() {
checkAuth = false;
}
-function setLoginState(bool) {
+function setLoginState(bool, id) {
Cookies.set('loginstate', bool, {
expires: 14
});
- if (loginStateChangeEvent && bool != lastLoginState)
- loginStateChangeEvent();
+ if (id) {
+ Cookies.set('userid', id, {
+ expires: 14
+ });
+ } else {
+ Cookies.remove('userid');
+ }
lastLoginState = bool;
+ lastUserId = id;
+ checkLoginStateChanged();
+}
+
+function checkLoginStateChanged() {
+ if (getLoginState() != lastLoginState || getUserId() != lastUserId) {
+ if(loginStateChangeEvent)
+ loginStateChangeEvent();
+ return true;
+ } else {
+ return false;
+ }
}
function getLoginState() {
return Cookies.get('loginstate') === "true";
}
+function getUserId() {
+ return Cookies.get('userid');
+}
+
function clearLoginState() {
Cookies.remove('loginstate');
}
function checkIfAuth(yesCallback, noCallback) {
var cookieLoginState = getLoginState();
+ if (checkLoginStateChanged())
+ checkAuth = false;
if (!checkAuth || typeof cookieLoginState == 'undefined') {
$.get('/me')
.done(function (data) {
if (data && data.status == 'ok') {
profile = data;
yesCallback(profile);
- setLoginState(true);
+ setLoginState(true, data.id);
} else {
noCallback();
setLoginState(false);
@@ -43,8 +67,10 @@ function checkIfAuth(yesCallback, noCallback) {
.fail(function () {
noCallback();
setLoginState(false);
+ })
+ .always(function () {
+ checkAuth = true;
});
- checkAuth = true;
} else if (cookieLoginState) {
yesCallback(profile);
} else {
diff --git a/public/js/cover.js b/public/js/cover.js
index 24ba605c..322768bf 100644
--- a/public/js/cover.js
+++ b/public/js/cover.js
@@ -229,6 +229,44 @@ var source = $("#template").html();
var template = Handlebars.compile(source);
var context = {
release: [
+ {
+ version: "0.2.9",
+ tag: "wildfire",
+ date: moment("201505301400", 'YYYYMMDDhhmm').fromNow(),
+ detail: [
+ {
+ title: "Features",
+ item: [
+ "+ Support text auto complete",
+ "+ Support cursor tag and random last name",
+ "+ Support online user list",
+ "+ Support show user info in blockquote"
+ ]
+ },
+ {
+ title: "Enhancements",
+ item: [
+ "* Added more code highlighting support",
+ "* Added more continue list support",
+ "* Adjust menu and history filter UI for better UX",
+ "* Adjust sync scoll animte to gain performance",
+ "* Change compression method of dynamic data",
+ "* Optimized render script"
+ ]
+ },
+ {
+ title: "Fixes",
+ item: [
+ "* Access history fallback might get wrong",
+ "* Sync scroll not accurate",
+ "* Sync scroll reach bottom range too much",
+ "* Detect login state change not accurate",
+ "* Detect editor focus not accurate",
+ "* Server not handle some editor events"
+ ]
+ }
+ ]
+ },
{
version: "0.2.8",
tag: "flame",
diff --git a/public/js/extra.js b/public/js/extra.js
index 05fa4704..495c5677 100644
--- a/public/js/extra.js
+++ b/public/js/extra.js
@@ -65,6 +65,7 @@ function finishView(view) {
try {
for (var i = 0; i < mathjaxdivs.length; i++) {
MathJax.Hub.Queue(["Typeset", MathJax.Hub, mathjaxdivs[i].innerHTML]);
+ MathJax.Hub.Queue(viewAjaxCallback);
$(mathjaxdivs[i]).removeClass("mathjax");
}
} catch(err) {
@@ -101,6 +102,18 @@ function finishView(view) {
//render title
document.title = renderTitle(view);
}
+
+//regex for blockquote
+var spaceregex = /\s*/;
+var notinhtmltagregex = /(?![^<]*>|[^<>]*<\/)/;
+var coloregex = /\[color=([#|\(|\)|\s|\,|\w]*)\]/;
+coloregex = new RegExp(coloregex.source + notinhtmltagregex.source, "g");
+var nameregex = /\[name=([-|_|\s|\w]*)\]/;
+var timeregex = /\[time=([:|,|+|-|\(|\)|\s|\w]*)\]/;
+var nameandtimeregex = new RegExp(nameregex.source + spaceregex.source + timeregex.source + notinhtmltagregex.source, "g");
+nameregex = new RegExp(nameregex.source + notinhtmltagregex.source, "g");
+timeregex = new RegExp(timeregex.source + notinhtmltagregex.source, "g");
+
//only static transform should be here
function postProcess(code) {
var result = $('
' + code + '
');
@@ -121,6 +134,20 @@ function postProcess(code) {
lis[i].setAttribute('class', 'task-list-item');
}
}
+ //blockquote
+ var blockquote = result.find("blockquote");
+ blockquote.each(function (key, value) {
+ var html = $(value).html();
+ html = html.replace(coloregex, '
');
+ html = html.replace(nameandtimeregex, '
$1 $2');
+ html = html.replace(nameregex, '
$1');
+ html = html.replace(timeregex, '
$1');
+ $(value).html(html);
+ });
+ var blockquotecolor = result.find("blockquote .color");
+ blockquotecolor.each(function (key, value) {
+ $(value).closest("blockquote").css('border-left-color', $(value).attr('data-color'));
+ });
return result;
}
@@ -195,7 +222,7 @@ function highlightRender(code, lang) {
if (/\=$/.test(lang)) {
var lines = result.value.split('\n');
var linenumbers = [];
- for (var i = 0; i < lines.length; i++) {
+ for (var i = 0; i < lines.length - 1; i++) {
linenumbers[i] = "
" + (i + 1) + "
";
}
var linegutter = "
" + linenumbers.join('\n') + "
";
diff --git a/public/js/history.js b/public/js/history.js
index 717a7ca4..c5db94c5 100644
--- a/public/js/history.js
+++ b/public/js/history.js
@@ -47,7 +47,7 @@ function saveHistoryToStorage(notehistory) {
if (store.enabled)
store.set('notehistory', JSON.stringify(notehistory));
else
- saveHistoryToCookie(notehistory);
+ saveHistoryToStorage(notehistory);
}
function saveHistoryToCookie(notehistory) {
@@ -146,11 +146,14 @@ function writeHistoryToServer(view) {
} catch (err) {
var notehistory = [];
}
+ if(!notehistory)
+ notehistory = [];
+
var newnotehistory = generateHistory(view, notehistory);
saveHistoryToServer(newnotehistory);
})
.fail(function () {
- writeHistoryToCookie(view);
+ writeHistoryToStorage(view);
});
}
@@ -160,7 +163,9 @@ function writeHistoryToCookie(view) {
} catch (err) {
var notehistory = [];
}
-
+ if(!notehistory)
+ notehistory = [];
+
var newnotehistory = generateHistory(view, notehistory);
saveHistoryToCookie(newnotehistory);
}
@@ -174,6 +179,9 @@ function writeHistoryToStorage(view) {
var notehistory = data;
} else
var notehistory = [];
+ if(!notehistory)
+ notehistory = [];
+
var newnotehistory = generateHistory(view, notehistory);
saveHistoryToStorage(newnotehistory);
} else {
@@ -241,7 +249,7 @@ function getServerHistory(callback) {
}
})
.fail(function () {
- getCookieHistory(callback);
+ getStorageHistory(callback);
});
}
@@ -282,7 +290,7 @@ function parseServerToHistory(list, callback) {
}
})
.fail(function () {
- parseCookieToHistory(list, callback);
+ parseStorageToHistory(list, callback);
});
}
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: '
\
+ \
+ \
+ \
+ \
+ '
+};
+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) {
$("
").insertAfter('.CodeMirror-cursors');
}
- if ($('#' + id).length <= 0) {
- var cursor = $('
');
- //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 = $('
');
+ 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 = $('
');
+ cursorbar[0].style.height = defaultTextHeight + 'px';
+ cursorbar[0].style.borderLeft = '2px solid ' + user.color;
+
+ var icon = '
';
+
+ var cursortag = $('
' + icon + ' ' + user.name + '
');
+ //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) {
+ $("