diff options
Diffstat (limited to 'trunk/etherpad/src/static/js/timeslider.js')
-rw-r--r-- | trunk/etherpad/src/static/js/timeslider.js | 663 |
1 files changed, 663 insertions, 0 deletions
diff --git a/trunk/etherpad/src/static/js/timeslider.js b/trunk/etherpad/src/static/js/timeslider.js new file mode 100644 index 0000000..552a971 --- /dev/null +++ b/trunk/etherpad/src/static/js/timeslider.js @@ -0,0 +1,663 @@ +/** + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + + + +function repeatString(str, times) { + if (times <= 0) return ""; + var s = repeatString(str, times >> 1); + s += s; + if (times & 1) s += str; + return s; +} +function chr(n) { return String.fromCharCode(n+48); } +function ord(c) { return c.charCodeAt(0)-48; } + +function map(array, func) { + var result = []; + // must remain compatible with "arguments" pseudo-array + for(var i=0;i<array.length;i++) { + if (func) result.push(func(array[i], i)); + else result.push(array[i]); + } + return result; +} + +function forEach(array, func) { + for(var i=0;i<array.length;i++) { + var result = func(array[i], i); + if (result) break; + } +} + +function getText(padOpaqueRef, r, func/*(text, optErrorData)*/) { + doAjaxGet('/ep/pad/history/'+padOpaqueRef+'/text/'+Number(r), + function(data, optErrorData) { + if (optErrorData) { + func(null, optErrorData); + } + else { + var text = data.text; + func({text: text}); + } + }); +} + +function getChanges(padOpaqueRef, first, last, func/*(data, optErrorData)*/) { + doAjaxGet('/ep/pad/history/'+padOpaqueRef+'/changes/'+Number(first)+'-'+Number(last), + function(data, optErrorData) { + if (optErrorData) { + func(null, optErrorData); + } + else { + func(uncompressChangesBlock({charPool: data.charPool, + changes: data.changes, + firstRev: first})); + } + }); +} + +function statPad(padOpaqueRef, func/*(atext, optErrorData)*/) { + doAjaxGet('/ep/pad/history/'+padOpaqueRef+'/stat', + function(data, optErrorData) { + if (optErrorData) { + func(null, optErrorData); + } + else { + var obj = {exists: data.exists}; + if (obj.exists) { + obj.latestRev = data.latestRev; + } + + func(obj); + } + }); +} + +function doAjaxGet(url, func/*(data, optErrorData)*/) { + $.ajax({ + type: 'get', + dataType: 'json', + url: url, + success: function(data) { + if (data.error) { + func(null, {serverError: data}); + } + else { + func(data); + } + }, + error: function(xhr, textStatus, errorThrown) { + func(null, {clientError: { textStatus:textStatus, errorThrown: errorThrown }}); + } + }); +} + +function uncompressChangesBlock(data) { + var charPool = data.charPool; + var changesArray = data.changes.split(','); + var firstRev = data.firstRev; + + var changesBlock = {}; + var changeStructs = []; + var charPoolIndex = 0; + var lastTimestamp = 0; + for(var i=0;i<changesArray.length;i++) { + var receiver = [null, 0]; + var curString = changesArray[i]; + function nextChar() { + return curString.charAt(receiver[1]); + } + function readChar() { + var c = nextChar(); + receiver[1]++; + return c; + } + function readNum() { + return decodeVarInt(curString, receiver[1], receiver); + } + function readString() { + var len = readNum(); + var str = charPool.substr(charPoolIndex, len); + charPoolIndex += len; + return str; + } + function readTimestamp() { + var absolute = false; + if (nextChar() == "+") { + readChar(); + absolute = true; + } + var t = readNum()*1000; + if (! absolute) { + t += lastTimestamp; + } + lastTimestamp = t; + return t; + } + function atEnd() { + return receiver[1] >= curString.length; + } + var timestamp = readTimestamp(); + var authorNum = readNum(); + var splices = []; + while (! atEnd()) { + var spliceType = readChar(); + var startChar = readNum(); + var oldText = ""; + var newText = ""; + if (spliceType != '+') { + oldText = readString(); + } + if (spliceType != '-') { + newText = readString(); + } + splices.push([startChar,oldText,newText]); + } + changeStructs.push({t:timestamp, a:authorNum, splices:splices}); + } + + changesBlock.firstRev = firstRev; + changesBlock.changeStructs = changeStructs; + + return changesBlock; +} + +var BASE64_DIGITS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._"; +var BASE64_DIGIT_TO_NUM = (function() { + var map = {}; + for(var i=0;i<BASE64_DIGITS.length;i++) { + map[BASE64_DIGITS.charAt(i)] = i; + } + return map; +})(); + +function decodeVarInt(stringIn, indexIn, numAndIndexOut) { + var n = 0; + var done = false; + var i = indexIn; + while (! done) { + var d = + BASE64_DIGIT_TO_NUM[stringIn.charAt(i++)]; + if (isNaN(d)) return -1; + if ((d & 32) == 0) { + done = true; + } + n = n*32 + (d & 31); + } + if (numAndIndexOut) { + numAndIndexOut[0] = n; + numAndIndexOut[1] = i; + } + return n; +} + +function escapeHTML(s) { + var re = /[&<>\n]/g; + if (! re.MAP) { + // persisted across function calls! + re.MAP = { + '&': '&', + '<': '<', + '>': '>', + '\n': '<br/>' + }; + } + return s.replace(re, function(c) { return re.MAP[c]; }); +} + +var padOpaqueRef = clientVars.padOpaqueRef; +var keyframes = []; // [rev, atext] pairs +var changesBlocks = []; // [first, last, changesBlock] +var lastRev; +var lastRevLoaded = -1; +var problemData = null; +var curRev = -1; +var curText = { lines: [/*string, length+1*/] }; + +function setLastRevLoaded(r) { + lastRevLoaded = r; + //$("#sliderui").slider('option', 'max', lastRevLoaded); + $("#currevdisplay .max").html(String(lastRevLoaded)); +} + +function initialStat(continuation) { + statPad(padOpaqueRef, function(data, errorData) { + if (errorData) { + reportProblem(errorData); + continuation(false); + } + else { + if (! data.exists) { + reportProblem({msg: "Pad not found."}); + continuation(false); + } + else { + lastRev = data.latestRev; + continuation(true); + return; + } + } + }); +} + +function loadKeyframe(r, continuation) { + getText(padOpaqueRef, r, function(data, errorData) { + if (errorData) { + reportProblem(errorData); + continuation(false); + } + else { + keyframes.push([r, data]); + keyframes.sort(function(a, b) { + return a[0] - b[0]; + }); + continuation(true); + } + }); +} + +function loadChangesBlock(first, last, continuation) { + getChanges(padOpaqueRef, first, last, function(data, errorData) { + if (errorData) { + reportProblem(errorData); + continuation(false); + } + else { + changesBlocks.push([first, last, data]); + continuation(true); + } + }); +} + +function loadThroughZero(continuation) { + initialStat(function(success) { + if (success) { + loadKeyframe(0, function(success) { + if (success) { + setLastRevLoaded(0); + continuation(true); + } + else continuation(false); + }); + } + else continuation(false); + }); +} + +function loadMoreRevs(continuation) { + if (lastRevLoaded >= lastRev) { + continuation(true); + } + else { + var first = lastRevLoaded+1; + var last = first + 499; + if (last > lastRev) { + last = lastRev; + } + loadChangesBlock(first, last, function(success) { + if (success) { + loadKeyframe(last, function(success) { + if (success) { + setLastRevLoaded(last); + continuation(true); + } + else continuation(false); + }); + } + else continuation(false); + }); + } +} + +function getDocTextForText(text) { + var lines = map(text.split('\n').slice(0, -1), function(s) { + return [s, s.length+1]; + }); + return { lines: lines }; +} + +function getLineAndChar(docText, charIndex) { + // returns [lineIndex, charIndexIntoLine]; + // if the charIndex is after the final newline of the document, + // lineIndex may be == docText.lines.length. + // Otherwise, lneIndex is an actual line and charIndex + // is between 0 and the line's length inclusive. + var startLine = 0; + var startLineStartChar = 0; + var lines = docText.lines; + var done = false; + while (!done) { + if (startLine >= lines.length) { + done = true; + } + else { + var lineLength = lines[startLine][1]; + var nextLineStart = startLineStartChar + lineLength; + if (nextLineStart <= charIndex) { + startLine++; + startLineStartChar = nextLineStart; + } + else { + done = true; + } + } + } + return [startLine, charIndex - startLineStartChar]; +} + +function applySplice(docText, splice, forward) { + var startChar = splice[0]; + var oldText = splice[1]; + var newText = splice[2]; + if (! forward) { + var tmp = oldText; + oldText = newText; + newText = tmp; + } + + //var OLD_FULL_TEXT = map(docText.lines, function(L) { return L[0]; }).join('\n')+'\n'; + //var OLD_NUM_LINES = docText.lines.length; + + var lines = docText.lines; + var startLineAndChar = getLineAndChar(docText, startChar); + var endLineAndChar = getLineAndChar(docText, startChar+oldText.length); + + var lineSpliceStart = startLineAndChar[0]; + var lineSpliceEnd = endLineAndChar[0]; + var newLines = newText.split('\n'); + // we want to splice in entire lines, so adjust start to include beginning of line + // we're starting to insert into + if (startLineAndChar[1] > 0) { + newLines[0] = lines[startLineAndChar[0]][0].substring(0, startLineAndChar[1]) + newLines[0]; + } + // adjust end to include entire last line that will be changed + if (endLineAndChar[1] > 0 || newLines[newLines.length-1].length > 0) { + newLines[newLines.length-1] += lines[endLineAndChar[0]][0].substring(endLineAndChar[1]); + lineSpliceEnd += 1; + } + else { + // the splice is ok as is, except for an extra newline + newLines.pop(); + } + + var newLineEntries = map(newLines, function(s) { + return [s, s.length+1]; + }); + + Array.prototype.splice.apply(lines, + [lineSpliceStart, lineSpliceEnd-lineSpliceStart].concat(newLineEntries)); + + // check it + //var EXPECTED_FULL_TEXT = OLD_FULL_TEXT.substring(0, startChar) + newText + + //OLD_FULL_TEXT.substring(startChar + oldText.length, OLD_FULL_TEXT.length); + //var ACTUAL_FULL_TEXT = map(docText.lines, function(L) { return L[0]; }).join('\n')+'\n'; + + //console.log("%o %o %o %d %d %d %d %d", + //docText.lines, startLineAndChar, endLineAndChar, OLD_NUM_LINES, + //lines.length, lineSpliceStart, lineSpliceEnd-lineSpliceStart, newLineEntries.length); + + //if (EXPECTED_FULL_TEXT != ACTUAL_FULL_TEXT) { + //console.log(escapeHTML("mismatch: "+EXPECTED_FULL_TEXT+" / "+ACTUAL_FULL_TEXT)); + //} + + return [lineSpliceStart, lineSpliceEnd-lineSpliceStart, newLines]; +} + +function lineHTML(line) { + return (escapeHTML(line) || ' '); +} + +function setCurText(docText, dontSetDom) { + curText = docText; + if (! dontSetDom) { + var docNode = $("#stuff"); + var html = map(docText.lines, function(line) { + return '<div>'+lineHTML(line[0])+'</div>'; + }); + docNode.html(html.join('')); + } +} + +function spliceDom(splice) { + var index = splice[0]; + var numRemoved = splice[1]; + var newLines = splice[2]; + + var overlap = Math.min(numRemoved, newLines.length); + var container = $("#stuff").get(0); + var oldNumNodes = container.childNodes.length; + var i = 0; + for(;i<overlap;i++) { + var n = container.childNodes.item(index+i); + $(n).html(lineHTML(newLines[i])); + } + for(;i<newLines.length;i++) { + var insertIndex = index+i; + var content = '<div>'+lineHTML(newLines[i])+'</div>'; + if (insertIndex >= container.childNodes.length) { + $(container).append(content); + } + else { + $(container.childNodes.item(insertIndex)).before(content); + } + } + for(;i<numRemoved;i++) { + var deleteIndex = index+overlap; + $(container.childNodes.item(deleteIndex)).remove(); + } + + //console.log("%d %d %d %d %d", splice[0], splice[1], splice[2].length, + //oldNumNodes + newLines.length - numRemoved, + //container.childNodes.length); +} + +function seekToRev(r) { + // precond: r is reachable + + var isStep = false; + + var bestKeyFrameIndex = -1; + var bestKeyFrameDistance = -1; + function considerKeyframe(index, kr) { + var dist = Math.abs(r - kr); + if (bestKeyFrameDistance < 0 || dist < bestKeyFrameDistance) { + bestKeyFrameDistance = dist; + bestKeyFrameIndex = index; + } + } + for(var i=0;i<keyframes.length;i++) { + considerKeyframe(i, keyframes[i][0]); + } + if (curRev >= 0) { + if (Math.abs(r - curRev) == 1) { + isStep = true; + bestKeyFrameIndex = -2; // -2 to mean "current revision" + } + else { + considerKeyframe(-2, curRev); + } + } + + var docText = curText; + var docRev = curRev; + if (bestKeyFrameIndex >= 0) { + // some keyframe is better than moving from the current location; + // move to that keyframe + var keyframe = keyframes[bestKeyFrameIndex]; + docRev = keyframe[0]; + docText = getDocTextForText(keyframe[1].text); + } + + var startRev = docRev; + var destRev = r; + + var curChangesBlock = null; + function findChangesBlockFor(n) { + function changesBlockWorks(arr) { + return n >= arr[0] && n <= arr[1]; + } + if (curChangesBlock == null || ! changesBlockWorks(curChangesBlock)) { + curChangesBlock = null; + for(var i=0;i<changesBlocks.length;i++) { + var cba = changesBlocks[i]; + if (changesBlockWorks(cba)) { + curChangesBlock = cba; + break; + } + } + } + } + + //var DEBUG_REVS_APPLIED = []; + + function applyRev(n, forward) { + findChangesBlockFor(n); + var cb = curChangesBlock[2]; + var idx = n - curChangesBlock[0]; + var chng = cb.changeStructs[idx]; + + var splices = chng.splices; + if (forward) { + for(var i=0;i<splices.length;i++) { + var splice = applySplice(docText, splices[i], true); + if (isStep) spliceDom(splice); + } + } + else { + for(var i=splices.length-1;i>=0;i--) { + var splice = applySplice(docText, splices[i], false); + if (isStep) spliceDom(splice); + } + } + + //DEBUG_REVS_APPLIED.push(n); + } + + if (destRev > startRev) { + for (var j=startRev+1; j<=destRev; j++) { + applyRev(j, true); + } + } + else if (destRev < startRev) { + for(var j=startRev; j >= destRev+1; j--) { + applyRev(j, false); + } + } + + docRev = destRev; + + setCurText(docText, isStep); + curRev = docRev; + $("#currevdisplay .cur").html(String(curRev)); +} + +function reportProblem(probData) { + problemData = probData; + if (probData.msg) { + $("#stuff").html(escapeHTML(probData.msg)); + } +} + +var playTimer = null; + +$(function() { + /*$("#sliderui").slider({min: 0, max: 0, value: 0, step: 1, change: slidechange}); + function slidechange(event, ui) { + alert("HELLO"); + var value = ui.value; + console.log(value); + }*/ + + $("#controls .next").click(function() { + if (curRev < lastRevLoaded) { + seekToRev(curRev+1); + } + return false; + }); + + $("#controls .prev").click(function() { + if (curRev > 0) { + seekToRev(curRev-1); + } + return false; + }); + + function stop() { + if (playTimer) { + clearInterval(playTimer); + playTimer = null; + } + } + + function play() { + stop(); + playTimer = setInterval(function() { + if (curRev < lastRevLoaded) { + seekToRev(curRev+1); + } + else { + stop(); + } + }, 60); + return false; + } + + $("#controls .play").click(play); + + $("#controls .stop").click(function() { + stop(); + return false; + }); + + $("#controls .entry").change(function() { + var value = $("#controls .entry").val(); + value = Number(value || 0); + if (isNaN(value)) value = 0; + if (value < 0) value = 0; + if (value > lastRevLoaded) { + value = lastRevLoaded; + } + $("#controls .entry").val(''); + seekToRev(value); + }); + $("#controls .entry").val(''); + + var useAutoplay = true; + var hasAutoplayed = false; + + loadThroughZero(function(success) { + if (success) { + seekToRev(0); + + function loadMoreRevsIfNecessary(continuation) { + if (lastRevLoaded < lastRev) { + loadMoreRevs(continuation); + } + } + loadMoreRevsIfNecessary(function cont(success) { + if (success) { + if (lastRevLoaded > 0 && useAutoplay && ! hasAutoplayed) { + hasAutoplayed = true; + play(); + } + loadMoreRevsIfNecessary(cont); + } + }); + } + }); +}); + |