@@ -4,6 +4,11 @@ let initiated = false;
44let last = undefined ;
55let 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+
712exports . 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) => {
2732exports . className2Author = ( className ) => {
2833 if ( className . substring ( 0 , 7 ) === 'author-' ) {
2934 return className . substring ( 7 ) . replace ( / [ a - y 0 - 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
7282exports . 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>￯</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
253291const 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