Skip to content

Commit d47961f

Browse files
authored
Merge pull request #2 from germanocaumo/tamo-patch
Fix performance issues with many edits +
2 parents b0f08d4 + 1859d27 commit d47961f

File tree

1 file changed

+102
-64
lines changed

1 file changed

+102
-64
lines changed

static/js/main.js

Lines changed: 102 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ let initiated = false;
44
let last = undefined;
55
let globalKey = 0;
66

7+
// queue & timer for throttling:
8+
let messageQueue = {}; // { authorId: payload }
9+
let processTimer = null;
10+
const PROCESS_DELAY = 50; // 50ms throttle window
11+
712
exports.aceInitInnerdocbodyHead = (hookName, args, cb) => {
813
const url = '../static/plugins/ep_cursortrace/static/css/ace_inner.css';
914
args.iframeHTML.push(`<link rel="stylesheet" type="text/css" href="${url}"/>`);
@@ -27,7 +32,9 @@ exports.getAuthorClassName = (author) => {
2732
exports.className2Author = (className) => {
2833
if (className.substring(0, 7) === 'author-') {
2934
return className.substring(7).replace(/[a-y0-9]+|-|z.+?z/g, (cc) => {
30-
if (cc === '-') { return '.'; } else if (cc.charAt(0) === 'z') {
35+
if (cc === '-') {
36+
return '.';
37+
} else if (cc.charAt(0) === 'z') {
3138
return String.fromCharCode(Number(cc.slice(1, -1)));
3239
} else {
3340
return cc;
@@ -41,9 +48,13 @@ exports.aceEditEvent = (hook_name, args) => {
4148
// null (no last cursor) and [line, col]
4249
// The AceEditEvent because it usually applies to selected items and isn't
4350
// really so mucha bout current position.
44-
const caretMoving = ((args.callstack.editEvent.eventType === 'handleClick') ||
45-
(args.callstack.type === 'handleKeyEvent') || (args.callstack.type === 'idleWorkTimer'));
46-
if (caretMoving && initiated) { // Note that we have to use idle timer to get the mouse position
51+
const caretMoving = (
52+
(args.callstack.editEvent.eventType === 'handleClick') ||
53+
(args.callstack.type === 'handleKeyEvent') ||
54+
(args.callstack.type === 'idleWorkTimer')
55+
);
56+
57+
if (caretMoving && initiated) {
4758
const Y = args.rep.selStart[0];
4859
const X = args.rep.selStart[1];
4960
if (!last || Y !== last[0] || X !== last[1]) { // If the position has changed
@@ -58,55 +69,81 @@ exports.aceEditEvent = (hook_name, args) => {
5869
padId,
5970
myAuthorId,
6071
};
61-
last = [];
62-
last[0] = Y;
63-
last[1] = X;
72+
last = [Y, X];
6473

6574
// console.log("Sent message", message);
66-
pad.collabClient.sendMessage(message); // Send the cursor position message to the server
75+
pad.collabClient.sendMessage(message);
6776
}
6877
}
6978
return;
7079
};
7180

81+
// Throttle the handleClientMessage_CUSTOM
7282
exports.handleClientMessage_CUSTOM = (hook, context, cb) => {
7383
/* I NEED A REFACTOR, please */
7484
// A huge problem with this is that it runs BEFORE the dom has
7585
// been updated so edit events are always late..
76-
7786
const action = context.payload.action;
7887
const authorId = context.payload.authorId;
79-
if (pad.getUserId() === authorId) return false;
8088
// Dont process our own caret position (yes we do get it..) -- This is not a bug
81-
const authorClass = exports.getAuthorClassName(authorId);
89+
if (pad.getUserId() === authorId) return false;
8290

8391
if (action === 'cursorPosition') {
84-
// an author has sent this client a cursor position, we need to show it in the dom
85-
let authorName = context.payload.authorName;
92+
// Queue this author's latest cursor data
93+
messageQueue[authorId] = context.payload;
94+
95+
// If not already scheduled, set a timer
96+
if (!processTimer) {
97+
processTimer = setTimeout(() => {
98+
processTimer = null;
99+
processQueuedMessages();
100+
}, PROCESS_DELAY);
101+
}
102+
}
103+
104+
return cb();
105+
};
106+
107+
// Process messages in bulk
108+
function processQueuedMessages() {
109+
const queued = { ...messageQueue };
110+
messageQueue = {}; // clear the queue
111+
112+
// For each author in the queue, run the DOM logic
113+
Object.keys(queued).forEach((authorId) => {
114+
const payload = queued[authorId];
115+
if (!payload) return;
116+
117+
const authorClass = exports.getAuthorClassName(authorId);
118+
119+
let authorName = payload.authorName;
86120
if (authorName === 'null' || authorName == null) {
87121
// If the users username isn't set then display a smiley face
88122
authorName = '😊';
89123
}
90124
// +1 as Etherpad line numbers start at 1
91-
const y = context.payload.locationY + 1;
92-
let x = context.payload.locationX;
125+
const y = payload.locationY + 1;
126+
let x = payload.locationX - 1;
127+
93128
const inner = $('iframe[name="ace_outer"]').contents().find('iframe');
94129
let leftOffset;
95130
if (inner.length !== 0) {
96-
leftOffset = parseInt($(inner).offset().left);
97-
leftOffset += parseInt($(inner).css('padding-left'));
131+
leftOffset = parseInt($(inner).offset().left) || 0;
132+
leftOffset += parseInt($(inner).css('padding-left')) || 0;
98133
}
99134

100135
let stickStyle = 'stickDown';
101136

102-
// Get the target Line
137+
// Get the target line
103138
const div = $('iframe[name="ace_outer"]').contents()
104-
.find('iframe').contents().find('#innerdocbody').find(`div:nth-child(${y})`);
139+
.find('iframe').contents().find('#innerdocbody')
140+
.find(`div:nth-child(${y})`);
105141

106-
const divWidth = div.width();
107142
// Is the line visible yet?
108143
if (div.length !== 0) {
109-
let top = $(div).offset().top; // A standard generic offset
144+
const divWidth = div.width();
145+
const divLineHeight = parseInt(getComputedStyle(div.get(0)).lineHeight);
146+
let top = parseInt($(div).offset().top) || 0; // A standard generic offset
110147
// The problem we have here is we don't know the px X offset of the caret from the user
111148
// Because that's a blocker for now lets just put a nice little div on the left hand side..
112149
// SO here is how we do this..
@@ -115,26 +152,32 @@ exports.handleClientMessage_CUSTOM = (hook, context, cb) => {
115152
// Delete everything after X chars
116153
// Measure the new width -- This gives us the offset without modifying the ACE Dom
117154
// Due to IE sucking this doesn't work in IE....
118-
119155
// We need the offset of the innerdocbody on top too.
120-
top += parseInt($('iframe[name="ace_outer"]').contents().find('iframe').css('paddingTop'));
156+
top += parseInt($('iframe[name="ace_outer"]').contents()
157+
.find('iframe').css('paddingTop')) || 0;
158+
// and the offset of the outerdocbody too. (for wide/narrow screens compatibility)
159+
top += parseInt($('iframe[name="ace_outer"]').contents().find('#outerdocbody')
160+
.css('padding-top')) - 10;
121161

122-
// Get the HTML
123-
const html = $(div).html();
162+
// Get the HTML, appending a dummy span to express the end of the line
163+
const html = $(div).html() + `<span>&#xFFEF;</span>`;
124164

125165
// build an ugly ID, makes sense to use authorId as authorId's cursor can only exist once
126166
const authorWorker = `hiddenUgly${exports.getAuthorClassName(authorId)}`;
127167

128168
// if Div contains block attribute IE h1 or H2 then increment by the number
129169
// This is horrible but a limitation because I'm parsing HTML
130-
if ($(div).children('span').length < 1) { x -= 1; }
170+
if ($(div).children('span').length < 1) {
171+
x -= 1;
172+
}
131173

132174
// Get the new string but maintain mark up
133-
const newText = html_substr(html, (x));
175+
const newText = html_substr(html, x);
134176

177+
// Insert a hidden measuring element
135178
// A load of ugly HTML that can prolly be moved to CSS
136-
const newLine = `<span style='width:${divWidth}px' id='${authorWorker}'` +
137-
` class='ghettoCursorXPos'>${newText}</span>`;
179+
const newLine = `<span style='width:${divWidth}px; ; line-height:${divLineHeight}px;'
180+
id='${authorWorker}' class='ghettoCursorXPos'>${newText}</span>`;
138181

139182
// Set the globalKey to 0, we use this when we wrap the objects in a datakey
140183
globalKey = 0; // It's bad, messy, don't ever develop like this.
@@ -144,17 +187,27 @@ exports.handleClientMessage_CUSTOM = (hook, context, cb) => {
144187

145188
// Get the worker element
146189
const worker = $('iframe[name="ace_outer"]').contents()
147-
.find('#outerdocbody').find(`#${authorWorker}`);
148-
190+
.find('#outerdocbody').find(`#${authorWorker}`);
149191
// Wrap the HTML in spans so we can find a char
150192
$(worker).html(wrap($(worker)));
151193
// console.log($(worker).html(), x);
152194

195+
// Copy relevant CSS from the line to match fonts
196+
const lineStyles = window.getComputedStyle(div[0]);
197+
worker.css({
198+
'font-size': lineStyles.fontSize,
199+
'font-family': lineStyles.fontFamily,
200+
'line-height': lineStyles.lineHeight,
201+
'white-space': lineStyles.whiteSpace,
202+
'font-weight': lineStyles.fontWeight,
203+
'letter-spacing': lineStyles.letterSpacing,
204+
});
205+
153206
// Get the Left offset of the x span
154207
const span = $(worker).find(`[data-key="${x - 1}"]`);
155208

156209
// Get the width of the element (This is how far out X is in px);
157-
let left;
210+
let left = 0;
158211
if (span.length !== 0) {
159212
left = span.position().left;
160213
} else {
@@ -168,35 +221,22 @@ exports.handleClientMessage_CUSTOM = (hook, context, cb) => {
168221
// plus the top offset minus the actual height of our focus span
169222
if (top <= 0) { // If the tooltip wont be visible to the user because it's too high up
170223
stickStyle = 'stickUp';
171-
top += (span.height() * 2);
172-
if (top < 0) { top = 0; } // handle case where caret is in 0,0
224+
top += (span.height() || 12) * 2;
225+
if (top < 0) top = 0; // handle case where caret is in 0,0
173226
}
174227

175228
// Add the innerdocbody offset
176-
left += leftOffset;
229+
left += leftOffset || 0;
177230

178231
// Add support for page view margins
179-
let divMargin = $(div).css('margin-left');
180-
let innerdocbodyMargin = $(div).parent().css('padding-left');
181-
if (innerdocbodyMargin) {
182-
innerdocbodyMargin = parseInt(innerdocbodyMargin);
183-
} else {
184-
innerdocbodyMargin = 0;
185-
}
186-
if (divMargin) {
187-
divMargin = divMargin.replace('px', '');
188-
// console.log("Margin is ", divMargin);
189-
divMargin = parseInt(divMargin);
190-
if ((divMargin + innerdocbodyMargin) > 0) {
191-
// console.log("divMargin", divMargin);
192-
left += divMargin;
193-
}
194-
}
232+
let divMargin = parseInt($(div).css('margin-left')) || 0;
233+
let innerdocbodyMargin = parseInt($(div).parent().css('padding-left')) || 0;
234+
left += (divMargin + innerdocbodyMargin);
195235
left += 18;
196236

197237
// Remove the element
198238
$('iframe[name="ace_outer"]').contents().find('#outerdocbody')
199-
.contents().remove(`#${authorWorker}`);
239+
.contents().remove(`#${authorWorker}`);
200240

201241
// Author color
202242
const users = pad.collabClient.getConnectedUsers();
@@ -217,20 +257,19 @@ exports.handleClientMessage_CUSTOM = (hook, context, cb) => {
217257

218258
// Create a new Div for this author
219259
const $indicator = $(`<div class='caretindicator caret-${authorClass}'
220-
style='height:16px; background-color:${color}'>
221-
</div>`);
260+
style='height:16px; background-color:${color}'>
261+
</div>`);
222262
const $paragraphName = $(`<p class='stickp'>${authorName}</p>`);
223-
263+
224264
//First insert elements into page to be able to use their widths to calculate when to switch stick to right
225265
$indicator.append($paragraphName);
226266
$(outBody).append($indicator);
227-
228-
const absolutePositionOfPageEnd = div.offset().left + div.width() + leftOffset + 2*divMargin;
229-
if(left > (absolutePositionOfPageEnd-$indicator.width())){
267+
268+
const absolutePositionOfPageEnd = div.offset().left + div.width() + leftOffset + 2 * divMargin;
269+
if (left > (absolutePositionOfPageEnd - $indicator.width())) {
230270
stickStyle = 'stickRight';
231-
left = left-$indicator.width();
271+
left = left - $indicator.width();
232272
}
233-
234273
$indicator.addClass(`${stickStyle}`);
235274
$paragraphName.addClass(`${stickStyle}`);
236275
$indicator.css('left', `${left}px`);
@@ -246,9 +285,8 @@ exports.handleClientMessage_CUSTOM = (hook, context, cb) => {
246285
}
247286
});
248287
}
249-
}
250-
return cb();
251-
};
288+
});
289+
}
252290

253291
const html_substr = (str, count) => {
254292
const div = document.createElement('div');
@@ -276,7 +314,7 @@ const html_substr = (str, count) => {
276314
} else if (node.nodeType === 1 && node.childNodes && node.childNodes[0]) {
277315
walk(node, fn);
278316
}
279-
} while (node = node.nextSibling); /* eslint-disable-line no-cond-assign */
317+
} while ((node = node.nextSibling)); /* eslint-disable-line no-cond-assign */
280318
};
281319
walk(div, track);
282320
return div.innerHTML;

0 commit comments

Comments
 (0)