Fix and improve cursor

pull/1/head
yflory 6 years ago
parent 1ba80a344b
commit f157f852b1

@ -0,0 +1,34 @@
.cursor_main() {
// CodeMirror
.cp-codemirror-cursor {
cursor: default;
background-color: red;
background-clip: padding-box;
padding: 0 1px;
border: 2px solid red;
border-right-color: transparent !important;
border-left-color: transparent !important;
}
.cp-codemirror-selection {
background-color: rgba(255,0,0,0.3);
}
// Tippy
.cp-cursor-avatar {
@size: 32px;
display: flex;
align-items: center;
media-tag {
min-height: @size;
max-height: @size;
min-width: @size;
max-width: @size;
margin-right: 10px;
img {
border-radius: 4px;
max-height: 100%;
max-width: 100%;
}
}
}
}

@ -13,6 +13,7 @@
@import (reference) "./app-print.less";
@import (reference) "./app-noscroll.less";
@import (reference) "./messenger.less";
@import (reference) "./cursor.less";
.framework_main(@bg-color, @warn-color, @color) {
--LessLoader_require: LessLoader_currentFile();
@ -38,6 +39,7 @@
.checkmark_main(20px);
.password_main();
.messenger_main();
.cursor_main();
.creation_main(
@bg-color: @bg-color,
@color: @color

@ -235,6 +235,7 @@
padding: 5px;
margin: 2px 0;
background: rgba(0,0,0,0.1);
border-right: 3px solid transparent;
.avatar_main(30px);
.cp-avatar-default, media-tag {
margin-right: 5px;

@ -9,13 +9,6 @@
@color: @colortheme_code-color
);
.cp-codemirror-cursor {
border-left: 2px solid red;
}
.cp-codemirror-selection {
background-color: rgba(255,0,0,0.3);
}
display: flex;
flex-flow: column;
max-height: 100%;

@ -304,7 +304,7 @@ define([
framework.onCursorUpdate(CodeMirror.setRemoteCursor);
framework.setCursorGetter(CodeMirror.getCursor);
editor.on('cursorActivity', function () {
if (editor._noCursorUpdate) { console.log('ok'); return; }
if (editor._noCursorUpdate) { return; }
framework.updateCursor();
});

@ -1211,6 +1211,13 @@ define([
var emojis = emojiStringToArray(str);
return isEmoji(emojis[0])? emojis[0]: str[0];
};
var avatars = {};
UIElements.setAvatar = function (hash, data) {
avatars[hash] = data;
};
UIElements.getAvatar = function (hash) {
return avatars[hash];
};
UIElements.displayAvatar = function (Common, $container, href, name, cb) {
var displayDefault = function () {
var text = getFirstEmojiOrCharacter(name);

@ -297,6 +297,15 @@ define([], function () {
return false;
};
Util.hexToRGB = function (hex) {
var h = hex.replace(/^#/, '');
return [
parseInt(h.slice(0,2), 16),
parseInt(h.slice(2,4), 16),
parseInt(h.slice(4,6), 16),
];
};
return Util;
});
}(self));

@ -417,6 +417,27 @@ define([
/////////////////////// Store ////////////////////////////////////
//////////////////////////////////////////////////////////////////
// Get or create the user color for the cursor position
var getRandomColor = function () {
var getColor = function () {
return Math.floor(Math.random() * 156) + 70;
};
return '#' + getColor().toString(16) +
getColor().toString(16) +
getColor().toString(16);
};
var getUserColor = function () {
var color = Util.find(store.proxy, ['settings', 'general', 'color']);
if (!color) {
color = getRandomColor();
Store.setAttribute(null, {
attr: ['general', 'color'],
value: color
}, function () {});
}
return color;
};
// Get the metadata for sframe-common-outer
Store.getMetadata = function (clientId, data, cb) {
var disableThumbnails = Util.find(store.proxy, ['settings', 'general', 'disableThumbnails']);
@ -427,6 +448,7 @@ define([
uid: store.proxy.uid,
avatar: Util.find(store.proxy, ['profile', 'avatar']),
profile: Util.find(store.proxy, ['profile', 'view']),
color: getUserColor(),
curvePublic: store.proxy.curvePublic,
},
// "priv" is not shared with other users but is needed by the apps

@ -0,0 +1,245 @@
define([
'/common/common-util.js',
'/common/common-constants.js',
'/customize/messages.js',
'/bower_components/chainpad-crypto/crypto.js',
], function (Util, Constants, Messages, Crypto) {
var Cursor = {};
var convertToUint8 = function (obj) {
var l = Object.keys(obj).length;
var u = new Uint8Array(l);
for (var i = 0; i<l; i++) {
u[i] = obj[i];
}
return u;
};
// Send the client's cursor to their channel when we receive an update
var sendMyCursor = function (ctx, clientId) {
var client = ctx.clients[clientId];
if (!client || !client.cursor) { return; }
var chan = ctx.channels[client.channel];
if (!chan) { return; }
var data = {
id: client.id,
cursor: client.cursor
};
chan.sendMsg(JSON.stringify(data));
ctx.emit('MESSAGE', data, chan.clients.filter(function (cl) {
return cl !== clientId;
}));
};
// Send all our cursors data when someone remote joins the channel
var sendOurCursors = function (ctx, chan) {
chan.clients.forEach(function (c) {
var client = ctx.clients[c];
if (!client) { return; }
var data = {
id: client.id,
cursor: client.cursor
};
// Send our data to the other users (NOT including the other tabs of the same worker)
chan.sendMsg(JSON.stringify(data));
});
};
var initCursor = function (ctx, obj, client, cb) {
var channel = obj.channel;
var secret = obj.secret;
if (secret.keys.cryptKey) {
secret.keys.cryptKey = convertToUint8(secret.keys.cryptKey);
}
var padChan = secret.channel;
var network = ctx.store.network;
var first = true;
var c = ctx.clients[client];
if (!c) {
c = ctx.clients[client] = {
channel: channel,
padChan: padChan,
cursor: {}
};
} else {
return void cb();
}
var chan = ctx.channels[channel];
if (chan) {
// This channel is already open in another tab
// ==> Set the ID to our client object
if (!c.id) { c.id = chan.wc.myID + '-' + client; }
// ==> Send the cursor position of the other tabs
chan.clients.forEach(function (cl) {
var clientObj = ctx.clients[cl];
if (!clientObj) { return; }
ctx.emit('MESSAGE', {
id: clientObj.id,
cursor: clientObj.cursor
}, [client]);
});
chan.sendMsg(JSON.stringify({join: true, id: c.id}));
// ==> And push the new tab to the list
chan.clients.push(client);
return void cb();
}
var onOpen = function (wc) {
ctx.channels[channel] = ctx.channels[channel] || {};
var chan = ctx.channels[channel];
if (!c.id) { c.id = wc.myID + '-' + client; }
if (chan.clients) {
// If 2 tabs from the same worker have been opened at the same time,
// we have to fix both of them
chan.clients.forEach(function (cl) {
if (ctx.clients[cl] && !ctx.clients[cl].id) {
ctx.clients[cl].id = wc.myID + '-' + cl;
}
});
}
chan.encryptor = Crypto.createEncryptor(secret.keys);
if (!chan.encryptor) { chan.encryptor = Crypto.createEncryptor(secret.keys); }
wc.on('join', function () {
sendOurCursors(ctx, chan);
});
wc.on('leave', function (peer) {
ctx.emit('MESSAGE', {leave: true, id: peer}, chan.clients);
});
wc.on('message', function (cryptMsg) {
var msg = chan.encryptor.decrypt(cryptMsg, secret.keys.validateKey);
var parsed;
try {
parsed = JSON.parse(msg);
if (parsed && parsed.join) {
return void sendOurCursors(ctx, chan);
}
ctx.emit('MESSAGE', parsed, chan.clients);
} catch (e) { console.error(e); }
});
chan.wc = wc;
chan.sendMsg = function (msg, cb) {
cb = cb || function () {};
var cmsg = chan.encryptor.encrypt(msg);
wc.bcast(cmsg).then(function () {
cb();
}, function (err) {
cb({error: err});
});
};
if (!first) { return; }
chan.clients = [client];
first = false;
cb();
};
network.join(channel).then(onOpen, function (err) {
return void cb({error: err});
});
network.on('reconnect', function () {
if (!ctx.channels[channel]) { console.log("cant reconnect", channel); return; }
network.join(channel).then(onOpen, function (err) {
console.error(err);
});
});
};
var updateCursor = function (ctx, data, client, cb) {
var c = ctx.clients[client];
if (!c) { return void cb({error: 'NO_CLIENT'}); }
data.color = Util.find(ctx.store.proxy, ['settings', 'general', 'color']);
data.name = ctx.store.proxy[Constants.displayNameKey] || Messages.anonymous;
data.avatar = Util.find(ctx.store.proxy, ['profile', 'avatar']);
console.log(data.color);
c.cursor = data;
sendMyCursor(ctx, client);
cb();
};
var leaveChannel = function (ctx, padChan) {
// Leave channel and prevent reconnect when we leave a pad
Object.keys(ctx.channels).some(function (cursorChan) {
var channel = ctx.channels[cursorChan];
if (channel.padChan !== padChan) { return; }
if (channel.wc) { channel.wc.leave(); }
delete ctx.channels[cursorChan];
return true;
});
};
// Remove the client from all its channels when a tab is closed
var removeClient = function (ctx, clientId) {
var filter = function (c) {
return c !== clientId;
};
// Remove the client from our channels
var chan;
for (var k in ctx.channels) {
chan = ctx.channels[k];
chan.clients = chan.clients.filter(filter);
if (chan.clients.length === 0) {
if (chan.wc) { chan.wc.leave(); }
delete ctx.channels[k];
}
}
// Send the leave message to the channel we were in
if (ctx.clients[clientId]) {
var leaveMsg = {
leave: true,
id: ctx.clients[clientId].id
};
chan = ctx.channels[ctx.clients[clientId].channel];
if (chan) {
chan.sendMsg(JSON.stringify(leaveMsg));
ctx.emit('MESSAGE', leaveMsg, chan.clients);
}
}
delete ctx.clients[clientId];
};
Cursor.init = function (store, emit) {
var cursor = {};
var ctx = {
store: store,
emit: emit,
channels: {},
clients: {}
};
cursor.removeClient = function (clientId) {
removeClient(ctx, clientId);
};
cursor.leavePad = function (padChan) {
leaveChannel(ctx, padChan);
};
cursor.execCommand = function (clientId, obj, cb) {
var cmd = obj.cmd;
var data = obj.data;
if (cmd === 'INIT_CURSOR') {
return void initCursor(ctx, data, clientId, cb);
}
if (cmd === 'UPDATE') {
return void updateCursor(ctx, data, clientId, cb);
}
};
return cursor;
};
return Cursor;
});

@ -393,6 +393,14 @@ define([
'class': 'cp-codemirror-cursor'
})[0];
};
var makeTippy = function (cursor) {
var html = '<span class="cp-cursor-avatar">';
if (cursor.avatar && UIElements.getAvatar(cursor.avatar)) {
html += UIElements.getAvatar(cursor.avatar);
}
html += cursor.name + '</span>';
return html;
};
var marks = {};
exp.setRemoteCursor = function (data) {
if (data.leave) {
@ -415,14 +423,30 @@ define([
delete marks[id];
}
if (!cursor.selectionStart) { return; }
if (cursor.selectionStart === cursor.selectionEnd) {
var cursorPosS = posToCursor(cursor.selectionStart, doc);
var el = makeCursor(id);
if (cursor.color) {
$(el).css('border-color', cursor.color);
$(el).css('background-color', cursor.color);
}
if (cursor.name) {
$(el).attr('title', cursor.name);
}
marks[id] = editor.setBookmark(cursorPosS, { widget: el });
} else {
var pos1 = posToCursor(cursor.selectionStart, doc);
var pos2 = posToCursor(cursor.selectionEnd, doc);
marks[id] = editor.markText(pos1, pos2, { className: 'cp-codemirror-selection' });
var css = cursor.color
? 'background-color: rgba(' + Util.hexToRGB(cursor.color).join(',') + ',0.2)'
: 'background-color: rgba(255,0,0,0.2)';
marks[id] = editor.markText(pos1, pos2, {
css: css,
title: makeTippy(cursor),
className: 'cp-tippy-html'
});
}
};

@ -0,0 +1,53 @@
define([
], function () {
var module = {};
module.create = function (Common) {
var exp = {};
var sframeChan = Common.getSframeChannel();
var metadataMgr = Common.getMetadataMgr();
var execCommand = function (cmd, data, cb) {
sframeChan.query('Q_CURSOR_COMMAND', {cmd: cmd, data: data}, function (err, obj) {
if (err || (obj && obj.error)) { return void cb(err || (obj && obj.error)); }
cb(void 0, obj);
});
};
exp.updateCursor = function (obj) {
execCommand('UPDATE', obj, function (err) {
if (err) { console.error(err); }
});
};
var messageHandlers = [];
exp.onCursorUpdate = function (handler) {
messageHandlers.push(handler);
};
var onMessage = function (data) {
messageHandlers.forEach(function (h) {
try {
h(data);
} catch (e) {
console.error(e);
}
});
};
sframeChan.on('EV_CURSOR_EVENT', function (obj) {
var cmd = obj.ev;
var data = obj.data;
if (cmd === 'MESSAGE') {
onMessage(data);
return;
}
});
return exp;
};
return module;
});

@ -232,6 +232,9 @@ MessengerUI, Messages) {
editUsersNames.forEach(function (data) {
var name = data.name || Messages.anonymous;
var $span = $('<span>', {'class': 'cp-avatar'});
if (data.color) {
$span.css('border-color', data.color);
}
var $rightCol = $('<span>', {'class': 'cp-toolbar-userlist-rightcol'});
var $nameSpan = $('<span>', {'class': 'cp-toolbar-userlist-name'}).appendTo($rightCol);
var $nameValue = $('<span>', {
@ -322,13 +325,13 @@ MessengerUI, Messages) {
window.open(origin+'/profile/#' + data.profile);
});
}
if (data.avatar && avatars[data.avatar]) {
$span.append(avatars[data.avatar]);
if (data.avatar && UIElements.getAvatar(data.avatar)) {
$span.append(UIElements.getAvatar(data.avatar));
$span.append($rightCol);
} else {
Common.displayAvatar($span, data.avatar, name, function ($img) {
if (data.avatar && $img && $img.length) {
avatars[data.avatar] = $img[0].outerHTML;
UIElements.setAvatar(data.avatar, $img[0].outerHTML);
}
$span.append($rightCol);
});

Loading…
Cancel
Save