Sunday, June 14, 2015

Synchro-scrolling three or more columns

I wanted to make a display that had three parallel windows. The left one would show a succession of page-images of some source document; the middle one an editable transcription of the document's entire text content in a MarkDown-like language; the third a rendition of that text as HTML. This gave the user the same information in three forms that were intrinsically out of sync with one another, with each column having a different height and layout of information. As you scroll down one column you would naturally like the other column to scroll in sync, so that at some point on the page – say the middle – would contain the same stuff and so the user would not lose his/her way. One attempt can be seen at the ecdosis Web site. Keeping track of how far down each page-number in the textarea is, and the corresponding positions in pixels down the columns that correspond in the other two views is an implementation detail I'll leave to the reader, although my code is available at that site. More than likely, however, you'd want to do that your own way.

The feedback problem

The key problem with all such displays is this: if I scroll column 2, and then set the scrollTop attribute of the other two columns, this will generate new secondary scroll events for columns 1 and 3 that are indistinguishable from the original event. In jQuery you can test the event.originalEvent field of the scroll event but it is mostly set to true even when it isn't an original event. The result is uncontrollable feedback. The display can freeze as each column talks to each other. One scrolls it down, the other slightly up, setting it vibrating. You can use the jQuery.scroll method but you have to surrender control of the event feedback again. The result is choppy and not at all smooth.

My solution is simple. All you do is set some global flag to the name of the currently scrolling column. Initially this is undefined, but on first scrolling say column 2, the "textarea", the global var scroller = "textarea". Now in the scroll handlers for the other two columns all you do is test if the current value of scroller is that of the relevant scroll event handler. (Of course your code will be different. This is just an example):

The view clicked on will always scroll by itself and prevent feedback by blocking the secondary scroll events (the calls to the specialised self.scrollTo method in the code above) when the scroll did not originate there. At the completion of scrolling the global (actually self.scroller, a variable in the containing object) is set back to undefined after a 200 millisecond delay. The reason for this is that Javascript is asynchronous. We cannot assume that when the current scroll handler has finished that the other scrolls have finished as well. So we set a timeout function to delay the reset to ensure that it happens after all of the current scroll is complete. Any more than 200 milliseconds and the user may have tried to click on another panel and found it blocked:

The timeout id resets itself when the timeout has completed. This is also used to prevent timeouts accumulating as the user scrolls continuously.