From d7c5ad7d6263fd1baf9bfdbaa4c50b70ef2fbdb2 Mon Sep 17 00:00:00 2001 From: Alexander Sulfrian Date: Tue, 8 Jun 2010 08:22:05 +0200 Subject: reverted folder structure change for better mergeing with upstream --- trunk/etherpad/src/static/js/ace.js | 29 + trunk/etherpad/src/static/js/billing.js | 111 + trunk/etherpad/src/static/js/billing_shared.js | 94 + trunk/etherpad/src/static/js/broadcast.js | 607 +++ .../etherpad/src/static/js/broadcast_revisions.js | 119 + trunk/etherpad/src/static/js/broadcast_slider.js | 401 ++ trunk/etherpad/src/static/js/collab_client.js | 628 +++ trunk/etherpad/src/static/js/colorutils.js | 91 + trunk/etherpad/src/static/js/confirmation.js | 21 + .../src/static/js/connection_diagnostics.js | 126 + trunk/etherpad/src/static/js/cssmanager_client.js | 88 + trunk/etherpad/src/static/js/domline_client.js | 210 + trunk/etherpad/src/static/js/draggable.js | 60 + trunk/etherpad/src/static/js/easysync2_client.js | 1777 ++++++++ trunk/etherpad/src/static/js/etherpad.js | 217 + trunk/etherpad/src/static/js/jquery-1.2.6.js | 3549 ++++++++++++++++ trunk/etherpad/src/static/js/jquery-1.3.2.js | 4376 ++++++++++++++++++++ trunk/etherpad/src/static/js/json2.js | 498 +++ .../src/static/js/lib/jquery.contextmenu.js | 284 ++ .../src/static/js/linestylefilter_client.js | 252 ++ trunk/etherpad/src/static/js/pad.js.old | 1984 +++++++++ trunk/etherpad/src/static/js/pad2.js | 591 +++ trunk/etherpad/src/static/js/pad_chat.js | 295 ++ .../etherpad/src/static/js/pad_connectionstatus.js | 63 + trunk/etherpad/src/static/js/pad_cookie.js | 101 + trunk/etherpad/src/static/js/pad_docbar.js | 347 ++ trunk/etherpad/src/static/js/pad_editbar.js | 107 + trunk/etherpad/src/static/js/pad_editor.js | 136 + trunk/etherpad/src/static/js/pad_impexp.js | 187 + trunk/etherpad/src/static/js/pad_modals.js | 364 ++ trunk/etherpad/src/static/js/pad_savedrevs.js | 408 ++ trunk/etherpad/src/static/js/pad_userlist.js | 604 +++ trunk/etherpad/src/static/js/pad_utils.js | 359 ++ trunk/etherpad/src/static/js/pricing.js | 19 + .../src/static/js/pro/guest-knock-client.js | 53 + .../src/static/js/pro/pro-padlist-client.js | 104 + trunk/etherpad/src/static/js/pro/signin-client.js | 27 + trunk/etherpad/src/static/js/pulse.jquery.js | 105 + trunk/etherpad/src/static/js/statpage.js | 143 + trunk/etherpad/src/static/js/store.js | 116 + trunk/etherpad/src/static/js/swfobject.js | 24 + trunk/etherpad/src/static/js/timeslider.js | 663 +++ trunk/etherpad/src/static/js/undo-xpopup.js | 25 + 43 files changed, 20363 insertions(+) create mode 100644 trunk/etherpad/src/static/js/ace.js create mode 100644 trunk/etherpad/src/static/js/billing.js create mode 100644 trunk/etherpad/src/static/js/billing_shared.js create mode 100644 trunk/etherpad/src/static/js/broadcast.js create mode 100644 trunk/etherpad/src/static/js/broadcast_revisions.js create mode 100644 trunk/etherpad/src/static/js/broadcast_slider.js create mode 100644 trunk/etherpad/src/static/js/collab_client.js create mode 100644 trunk/etherpad/src/static/js/colorutils.js create mode 100644 trunk/etherpad/src/static/js/confirmation.js create mode 100644 trunk/etherpad/src/static/js/connection_diagnostics.js create mode 100644 trunk/etherpad/src/static/js/cssmanager_client.js create mode 100644 trunk/etherpad/src/static/js/domline_client.js create mode 100644 trunk/etherpad/src/static/js/draggable.js create mode 100644 trunk/etherpad/src/static/js/easysync2_client.js create mode 100644 trunk/etherpad/src/static/js/etherpad.js create mode 100755 trunk/etherpad/src/static/js/jquery-1.2.6.js create mode 100644 trunk/etherpad/src/static/js/jquery-1.3.2.js create mode 100644 trunk/etherpad/src/static/js/json2.js create mode 100644 trunk/etherpad/src/static/js/lib/jquery.contextmenu.js create mode 100644 trunk/etherpad/src/static/js/linestylefilter_client.js create mode 100644 trunk/etherpad/src/static/js/pad.js.old create mode 100644 trunk/etherpad/src/static/js/pad2.js create mode 100644 trunk/etherpad/src/static/js/pad_chat.js create mode 100644 trunk/etherpad/src/static/js/pad_connectionstatus.js create mode 100644 trunk/etherpad/src/static/js/pad_cookie.js create mode 100644 trunk/etherpad/src/static/js/pad_docbar.js create mode 100644 trunk/etherpad/src/static/js/pad_editbar.js create mode 100644 trunk/etherpad/src/static/js/pad_editor.js create mode 100644 trunk/etherpad/src/static/js/pad_impexp.js create mode 100644 trunk/etherpad/src/static/js/pad_modals.js create mode 100644 trunk/etherpad/src/static/js/pad_savedrevs.js create mode 100644 trunk/etherpad/src/static/js/pad_userlist.js create mode 100644 trunk/etherpad/src/static/js/pad_utils.js create mode 100644 trunk/etherpad/src/static/js/pricing.js create mode 100644 trunk/etherpad/src/static/js/pro/guest-knock-client.js create mode 100644 trunk/etherpad/src/static/js/pro/pro-padlist-client.js create mode 100644 trunk/etherpad/src/static/js/pro/signin-client.js create mode 100644 trunk/etherpad/src/static/js/pulse.jquery.js create mode 100644 trunk/etherpad/src/static/js/statpage.js create mode 100644 trunk/etherpad/src/static/js/store.js create mode 100644 trunk/etherpad/src/static/js/swfobject.js create mode 100644 trunk/etherpad/src/static/js/timeslider.js create mode 100644 trunk/etherpad/src/static/js/undo-xpopup.js (limited to 'trunk/etherpad/src/static/js') diff --git a/trunk/etherpad/src/static/js/ace.js b/trunk/etherpad/src/static/js/ace.js new file mode 100644 index 0000000..6766cee --- /dev/null +++ b/trunk/etherpad/src/static/js/ace.js @@ -0,0 +1,29 @@ +Ace2Editor.registry={nextId:1};function Ace2Editor(){var K="Ace2Editor";var F=Ace2Editor;var B={};var A={editor:B,id:(F.registry.nextId++)}; +var D=false;var E=[];function C(R,Q){return function(){var T=this;var S=arguments;function U(){R.apply(T,S); +}if(Q){Q.apply(T,S);}if(D){U();}else{E.push(U);}};}function I(){for(var Q=0;Q'; +};var J=function(Q){return'\x3cscript type="text/javascript" src="'+Q+'">\x3c/script>';};var M=J;var N=H; +var L=function(Q){return'\''";};var G=function(Q){return'\'\\x3cscript type="text/javascript" src="'+Q+"\">\\x3c/script>'"; +};var P=G;var O=L;B.destroy=C(function(){A.ace_dispose();A.frame.parentNode.removeChild(A.frame);delete F.registry[A.id]; +A=null;});B.init=function(Q,S,R){B.importText(S);A.onEditorReady=function(){D=true;I();R();};(function(){var W=''; +var T=["'"+W+"'"];T.push(("('\\n\'');T.push('\' \''); +var X='editorId = "'+A.id+'"; editorInfo = parent.'+K+'.registry[editorId]; window.onload = function() { window.onload = null; setTimeout(function() { var iframe = document.createElement("IFRAME"); iframe.scrolling = "no"; var outerdocbody = document.getElementById("outerdocbody"); iframe.frameBorder = 0; iframe.allowTransparency = true; outerdocbody.insertBefore(iframe, outerdocbody.firstChild); iframe.ace_outerWin = window; readyFunc = function() { editorInfo.onEditorReady(); readyFunc = null; editorInfo = null; }; var doc = iframe.contentWindow.document; doc.open(); doc.write('+T.join("+")+"); doc.close(); }, 0); }"; +var Y=[W,"",(''),'',"\x3cscript>",X,"\x3c/script>",'
x
']; +var U=document.createElement("IFRAME");U.frameBorder=0;A.frame=U;document.getElementById(Q).appendChild(U); +var V=U.contentWindow.document;V.open();V.write(Y.join(""));V.close();B.adjustSize();})();};return B; +} \ No newline at end of file diff --git a/trunk/etherpad/src/static/js/billing.js b/trunk/etherpad/src/static/js/billing.js new file mode 100644 index 0000000..c9fa30e --- /dev/null +++ b/trunk/etherpad/src/static/js/billing.js @@ -0,0 +1,111 @@ +/** + * 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() { + billing.initFieldDisplay(); + billing.initCcValidation(); +}); + +billing.initFieldDisplay = function() { + var id = $('#billingselect input:checked').attr("value"); + $('.billingfield').not('.billingfield.'+id+'req').hide(); + $('.paymentbutton').click(billing.selectPaymentType); + + $('#billingCountry').click(billing.selectCountry); + billing.selectCountry(); +} + +billing.selectCountry = function() { + var countryCode = $('#billingCountry').attr("value"); + var id = $('#billingselect input:checked').attr("value"); + if (countryCode != 'US') { + $('.billingfield.intonly.'+id+'req').show(); + $('.billingfield.usonly').hide(); + } else { + $('.billingfield.intonly').hide(); + $('.billingfield.usonly.'+id+'req').show(); + } +} + +billing.countryAntiSelector = function() { + var countryCode = $('#billingCountry').attr("value"); + if (countryCode != 'US') { + return '.usonly'; + } else { + return '.intonly'; + } +} + +billing.selectPaymentType = function() { + var radio = $(this).children('input'); + var id = radio.attr("value"); + radio.attr("checked", "checked"); + + var selector = billing.countryAntiSelector(); + var toShow = $('.billingfield.'+id+'req:hidden').not('.billingfield'+selector); + var toHide = $('.billingfield:visible').not('.billingfield.'+id+'req'); + + if (toShow.size() > 0 && toHide.size() > 0) { + toHide.fadeOut(200); + setTimeout(function() { + toShow.fadeIn(200); + }, 200); + } else if (toShow.size() > 0 || toHide.size() > 0){ + toShow.fadeIn(200); + toHide.fadeOut(200); + } +} + +billing.extractCcType = function(numsrc) { + var number = $(numsrc).val(); + var newType = billing.getCcType(number); + $('.ccimage').removeClass('ccimageselected'); + if (newType) { + $('#img'+newType).addClass('ccimageselected'); + } + if (billing.validateCcNumber(number)) { + $('input[name=billingCCNumber]').css('border', '1px solid #0f0'); + } else if (billing.validateCcLength(number) || + ! (/^\d*$/.test(number))) { + $('input[name=billingCCNumber]').css('border', '1px solid #f00'); + } else { + $('input[name=billingCCNumber]').css('border', '1px solid black'); + } +} + +billing.handleCcFieldChange = function(target, event) { + if (event && + ! (event.keyCode == 8 || + (event.keyCode >= 32 && event.keyCode <= 126))) { + return; + } + var ccValue = $(target).val(); + if (ccValue == billing.lastCcValue) { + return; + } + billing.lastCcValue = ccValue; + setTimeout(function() { + billing.extractCcType(target); + }, 0); +} + +billing.initCcValidation = function() { + $('input[name=billingCCNumber]').keydown( + function(event) { billing.handleCcFieldChange(this, event); }); + $('input[name=billingCCNumber]').blur( + function() { billing.handleCcFieldChange(this) }); + billing.lastCcValue = $('input[name=billingCCNumber]').val(); +} \ No newline at end of file diff --git a/trunk/etherpad/src/static/js/billing_shared.js b/trunk/etherpad/src/static/js/billing_shared.js new file mode 100644 index 0000000..dc3a00c --- /dev/null +++ b/trunk/etherpad/src/static/js/billing_shared.js @@ -0,0 +1,94 @@ +/** + * 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. + */ + +var billing = {}; + +billing.CC = function(shortName, prefixes, length) { + this.type = shortName; + this.prefixes = prefixes; + this.length = length; + function validateLuhn(number) { + var digits = []; + var sum = 0; + for (var i = 0; i < number.length; ++i) { + var c = Number(number.charAt(number.length-1-i)); + sum += c; + if (i % 2 == 1) { // every second digit + sum += c; + if (2*c >= 10) { + sum -= 9; + } + } + } + return (sum % 10 == 0); + } + this.validatePrefix = function(number) { + for (var i = 0; i < this.prefixes.length; ++i) { + if (number.indexOf(String(this.prefixes[i])) == 0) { + return true; + } + } + return false; + } + this.validateLength = function(number) { + return number.length == this.length; + } + + this.validateNumber = function(number) { + return this.validateLength(number) && + this.validatePrefix(number) && + validateLuhn(number); + } +} + +billing.ccTypes = [ + new billing.CC('amex', [34, 37], 15), + new billing.CC('disc', [6011, 644, 645, 646, 647, 648, 649, 65], 16), + new billing.CC('mc', [51, 52, 53, 54, 55], 16), + new billing.CC('visa', [4], 16)]; + +billing.validateCcNumber = function(number) { + if (! (/^\d+$/.test(number))) { + return false; + } + for (var i = 0; i < billing.ccTypes.length; ++i) { + var ccType = billing.ccTypes[i]; + if (ccType.validatePrefix(number)) { + return ccType.validateNumber(number); + } + } + return false; +} + +billing.validateCcLength = function(number) { + for (var i = 0; i < billing.ccTypes.length; ++i) { + var ccType = billing.ccTypes[i]; + if (ccType.validatePrefix(number)) { + return ccType.validateLength(number); + } + } + return false; +} + +billing.getCcType = function(number) { + for (var i = 0; i < billing.ccTypes.length; ++i) { + var ccType = billing.ccTypes[i]; + if (ccType.validatePrefix(number)) { + return ccType.type; + } + } + return false; +} diff --git a/trunk/etherpad/src/static/js/broadcast.js b/trunk/etherpad/src/static/js/broadcast.js new file mode 100644 index 0000000..9fa8141 --- /dev/null +++ b/trunk/etherpad/src/static/js/broadcast.js @@ -0,0 +1,607 @@ +/** + * 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. + */ + +// just in case... (todo: this must be somewhere else in the client code.) +if (!Array.prototype.map) +{ + Array.prototype.map = function(fun /*, thisp*/) + { + var len = this.length >>> 0; + if (typeof fun != "function") + throw new TypeError(); + + var res = new Array(len); + var thisp = arguments[1]; + for (var i = 0; i < len; i++) + { + if (i in this) + res[i] = fun.call(thisp, this[i], i, this); + } + + return res; + }; +} + +if (!Array.prototype.forEach) +{ + Array.prototype.forEach = function(fun /*, thisp*/) + { + var len = this.length >>> 0; + if (typeof fun != "function") + throw new TypeError(); + + var thisp = arguments[1]; + for (var i = 0; i < len; i++) + { + if (i in this) + fun.call(thisp, this[i], i, this); + } + }; +} + +if (!Array.prototype.indexOf) +{ + Array.prototype.indexOf = function(elt /*, from*/) + { + var len = this.length >>> 0; + + var from = Number(arguments[1]) || 0; + from = (from < 0) + ? Math.ceil(from) + : Math.floor(from); + if (from < 0) + from += len; + + for (; from < len; from++) + { + if (from in this && + this[from] === elt) + return from; + } + return -1; + }; +} + +function debugLog() { + try { + // console.log.apply(console, arguments); + } catch (e) {console.log("error printing: ",e);} +} + +function randomString() { + return "_"+Math.floor(Math.random() * 1000000); +} + +// for IE +if ($.browser.msie) { + try { + document.execCommand("BackgroundImageCache", false, true); + } catch (e) {} +} + +var userId = "hiddenUser" + randomString(); +var socketId; +var socket; + +var channelState = "DISCONNECTED"; + +var appLevelDisconnectReason = null; + +var padContents = { + currentRevision: clientVars.revNum, + currentTime : clientVars.currentTime, + currentLines: Changeset.splitTextLines(clientVars.initialStyledContents.atext.text), + currentDivs : null, // to be filled in once the dom loads + apool: (new AttribPool()).fromJsonable(clientVars.initialStyledContents.apool), + alines: Changeset.splitAttributionLines( + clientVars.initialStyledContents.atext.attribs, + clientVars.initialStyledContents.atext.text), + + // generates a jquery element containing HTML for a line + lineToElement: function(line, aline) { + var element = document.createElement("div"); + var emptyLine = (line == '\n'); + var domInfo = domline.createDomLine(! emptyLine, true); + linestylefilter.populateDomLine(line, aline, this.apool, + domInfo); + domInfo.prepareForAdd(); + element.className = domInfo.node.className; + element.innerHTML = domInfo.node.innerHTML; + element.id = Math.random(); + return $(element); + }, + + applySpliceToDivs: function(start, numRemoved, newLines) { + // remove spliced-out lines from DOM + for(var i=start; i 10000) { + var start = (Math.floor((newRevision) / 10000) * 10000); // revision 0 to 10 + changesetLoader.queueUp(start, 100); + } + + if(BroadcastSlider.getSliderLength() > 1000) { + var start = (Math.floor((newRevision) / 1000) * 1000); // (start from -1, go to 19) + 1 + changesetLoader.queueUp(start, 10); + } + + start = (Math.floor((newRevision) / 100) * 100); + + changesetLoader.queueUp(start, 1, update); + } + BroadcastSlider.setAuthors(padContents.getActiveAuthors().map(function(name) {return authorData[name];})); +} + +var changesetLoader = { + running: false, + resolved: [], + requestQueue1: [], + requestQueue2: [], + requestQueue3: [], + queueUp: function(revision, width, callback) { + if(revision < 0) revision = 0; + // if(changesetLoader.requestQueue.indexOf(revision) != -1) + // return; // already in the queue. + if(changesetLoader.resolved.indexOf(revision+"_"+width) != -1) + return; // already loaded from the server + changesetLoader.resolved.push(revision+"_"+width); + + var requestQueue = width == 1 ? changesetLoader.requestQueue3 : + width == 10 ? changesetLoader.requestQueue2 : + changesetLoader.requestQueue1; + requestQueue.push({'rev': revision, 'res': width, 'callback': callback}); + if(!changesetLoader.running) { + changesetLoader.running = true; + setTimeout(changesetLoader.loadFromQueue, 10); + } + }, + loadFromQueue: function() { + var self = changesetLoader; + var requestQueue = self.requestQueue1.length > 0 ? self.requestQueue1 : + self.requestQueue2.length > 0 ? self.requestQueue2 : + self.requestQueue3.length > 0 ? self.requestQueue3 : null; + + if(!requestQueue) { + self.running = false; + return; + } + + var request = requestQueue.pop(); + var granularity = request.res; + var callback = request.callback; + var start = request.rev; + debugLog("loadinging revision", start, "through ajax"); + $.getJSON( + "/ep/pad/changes/"+clientVars.padIdForUrl+"?s="+start + "&g="+granularity, + function(data, textStatus) { + if(textStatus !== "success") { + console.log(textStatus); + BroadcastSlider.showReconnectUI(); + } + self.handleResponse(data, start, granularity, callback); + + setTimeout(self.loadFromQueue, 10); // load the next ajax function + } + ); + }, + handleResponse: function(data, start, granularity, callback) { + debugLog("response: ", data); + var pool = (new AttribPool()).fromJsonable(data.apool); + for(var i=0; i data.actualEndNum - 1) aend = data.actualEndNum - 1; + debugLog("adding changeset:", astart, aend); + var forwardcs = Changeset.moveOpsToNewPool(data.forwardsChangesets[i], pool, padContents.apool); + var backwardcs = Changeset.moveOpsToNewPool(data.backwardsChangesets[i], pool, padContents.apool); + revisionInfo.addChangeset(astart, aend, forwardcs, backwardcs, data.timeDeltas[i]); + } + if(callback)callback(start - 1, start + data.forwardsChangesets.length * granularity - 1); + } +}; + +function handleMessageFromServer() { + debugLog("handleMessage:", arguments); + var obj = arguments[0]['data']; + var expectedType = "COLLABROOM"; + + obj = JSON.parse(obj); + if (obj['type'] == expectedType) { + obj = obj['data']; + + if (obj['type'] == "NEW_CHANGES") { + debugLog(obj); + var changeset = Changeset.moveOpsToNewPool( + obj.changeset, (new AttribPool()).fromJsonable(obj.apool), + padContents.apool); + + var changesetBack = Changeset.moveOpsToNewPool( + obj.changesetBack, (new AttribPool()).fromJsonable(obj.apool), + padContents.apool); + + loadedNewChangeset(changeset, changesetBack, obj.newRev-1, obj.timeDelta); + } + else if (obj['type'] == "NEW_AUTHORDATA") { + var authorMap = {}; + authorMap[obj.author] = obj.data; + receiveAuthorData(authorMap); + BroadcastSlider.setAuthors(padContents.getActiveAuthors().map(function(name) {return authorData[name];})); + } else if (obj['type'] == "NEW_SAVEDREV") { + var savedRev = obj.savedRev; + BroadcastSlider.addSavedRevision(savedRev.revNum, savedRev); + } + } else { + debugLog("incorrect message type: " + obj['type'] + ", expected " + expectedType); + } +} + +function handleSocketClosed(params) { + debugLog("socket closed!", params); + socket = null; + + BroadcastSlider.showReconnectUI(); + // var reason = appLevelDisconnectReason || params.reason; + // var shouldReconnect = params.reconnect; + // if (shouldReconnect) { + // // determine if this is a tight reconnect loop due to weird connectivity problems + // // reconnectTimes.push(+new Date()); + // var TOO_MANY_RECONNECTS = 8; + // var TOO_SHORT_A_TIME_MS = 10000; + // if (reconnectTimes.length >= TOO_MANY_RECONNECTS && + // ((+new Date()) - reconnectTimes[reconnectTimes.length-TOO_MANY_RECONNECTS]) < + // TOO_SHORT_A_TIME_MS) { + // setChannelState("DISCONNECTED", "looping"); + // } + // else { + // setChannelState("RECONNECTING", reason); + // setUpSocket(); + // } + // } + // else { + // BroadcastSlider.showReconnectUI(); + // setChannelState("DISCONNECTED", reason); + // } +} + +function sendMessage(msg) { + socket.postMessage(JSON.stringify({type: "COLLABROOM", data: msg})); +} + +function setUpSocket() { + // required for Comet + if ((! $.browser.msie) && + (! ($.browser.mozilla && $.browser.version.indexOf("1.8.") == 0))) { + document.domain = document.domain; // for comet + } + + var success = false; + callCatchingErrors("setUpSocket", function() { + appLevelDisconnectReason = null; + + socketId = String(Math.floor(Math.random()*1e12)); + socket = new WebSocket(socketId); + socket.onmessage = wrapRecordingErrors("socket.onmessage", handleMessageFromServer); + socket.onclosed = wrapRecordingErrors("socket.onclosed", handleSocketClosed); + socket.onopen = wrapRecordingErrors("socket.onopen", function() { + setChannelState("CONNECTED"); + var msg = { type:"CLIENT_READY", roomType:'padview', + roomName:'padview/'+clientVars.viewId, + data: { lastRev:clientVars.revNum, + userInfo:{userId: userId} } }; + sendMessage(msg); + }); + // socket.onhiccup = wrapRecordingErrors("socket.onhiccup", handleCometHiccup); + // socket.onlogmessage = function(x) {debugLog(x); }; + socket.connect(); + success = true; + }); + if (success) { + //initialStartConnectTime = +new Date(); + } + else { + abandonConnection("initsocketfail"); + } +} + +function setChannelState(newChannelState, moreInfo) { + if (newChannelState != channelState) { + channelState = newChannelState; + // callbacks.onChannelStateChange(channelState, moreInfo); + } +} + +function abandonConnection(reason) { + if (socket) { + socket.onclosed = function() {}; + socket.onhiccup = function() {}; + socket.disconnect(); + } + socket = null; + setChannelState("DISCONNECTED", reason); +} + +window['onloadFuncts'] = []; +window.onload = function() { + window['isloaded'] = true; + window['onloadFuncts'].forEach(function(funct) { + funct(); + }); +}; + +// to start upon window load, just push a function onto this array +window['onloadFuncts'].push(setUpSocket); +window['onloadFuncts'].push(function() { + // set up the currentDivs and DOM + padContents.currentDivs = []; + $("#padcontent").html(""); + for(var i=0; i 0) { + goToRevisionIfEnabledCount --; + } else { + goToRevision.apply(goToRevision, arguments); + } +} + +BroadcastSlider.onSlider(goToRevisionIfEnabled); + +(function() { + for(var i=0; i revisionInfo.latest) { + revisionInfo.latest = index; + } + + return revisionInfo[index]; +} + +// assuming that there is a path from fromIndex to toIndex, and that the links +// are laid out in a skip-list format +revisionInfo.getPath = function(fromIndex, toIndex) { + var changesets = []; + var spans = []; + var times = []; + var elem = revisionInfo[fromIndex] || revisionInfo.createNew(fromIndex); + if(elem.changesets.length != 0 && fromIndex != toIndex) { + var reverse = !(fromIndex < toIndex) + while(((elem.rev < toIndex) && !reverse) || + ((elem.rev > toIndex) && reverse)) { + var couldNotContinue = false; + var oldRev = elem.rev; + + for(var i = reverse ? elem.changesets.length - 1 : 0; + reverse?i>=0:i 0) && reverse)) { + couldNotContinue = true; + break; + } + + if(((elem.rev + elem.changesets[i].deltaRev <= toIndex) && !reverse) || + ((elem.rev + elem.changesets[i].deltaRev >= toIndex) && reverse)) { + var topush = elem.changesets[i]; + changesets.push(topush.getValue()); + spans.push(elem.changesets[i].deltaRev); + times.push(topush.deltaTime); + elem = revisionInfo[elem.rev + elem.changesets[i].deltaRev]; + break; + } + } + + if(couldNotContinue || oldRev == elem.rev) break; + } + } + + var status = 'partial'; + if(elem.rev == toIndex) + status = 'complete'; + + return { + 'fromRev':fromIndex, + 'rev': elem.rev, + 'status': status, + 'changesets': changesets, + 'spans' : spans, + 'times' : times + }; +} + +// revisionInfo.addChangeset(0, 5, "abcde") +// revisionInfo.addChangeset(5, 10, "fghij") +// revisionInfo.addChangeset(10, 11, "k") +// revisionInfo.addChangeset(11, 12, "l") +// revisionInfo.addChangeset(12, 13, "m") +// revisionInfo.addChangeset(13, 14, "n") +// revisionInfo.addChangeset(14, 15, "o") +// revisionInfo.addChangeset(15, 20, "pqrst") +// +// print (revisionInfo.getPath(15, 0)) diff --git a/trunk/etherpad/src/static/js/broadcast_slider.js b/trunk/etherpad/src/static/js/broadcast_slider.js new file mode 100644 index 0000000..371663e --- /dev/null +++ b/trunk/etherpad/src/static/js/broadcast_slider.js @@ -0,0 +1,401 @@ +/** + * 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. + */ + +var global = this; + +(function() { // wrap this code in its own namespace + var sliderLength = 1000; + var sliderPos = 0; + var sliderActive = false; + var slidercallbacks = []; + var savedRevisions = []; + var sliderPlaying = false; + + function disableSelection(element) { + element.onselectstart = function() { + return false; + }; + element.unselectable = "on"; + element.style.MozUserSelect = "none"; + element.style.cursor = "default"; + } + var _callSliderCallbacks = function(newval) { + sliderPos = newval; + for(var i=0; i'); + newSavedRevision.addClass("star"); + + newSavedRevision.attr('pos', position); + newSavedRevision.css('position', 'absolute'); + newSavedRevision.css('left', (position * ($("#ui-slider-bar").width()-2) / (sliderLength * 1.0)) - 1); + $("#timeslider-slider").append(newSavedRevision); + newSavedRevision.mouseup(function(evt) { + BroadcastSlider.setSliderPosition(position); + }); + savedRevisions.push(newSavedRevision); + }; + + var removeSavedRevision = function (position) { + var element = $("div.star [pos="+position+"]"); + savedRevisions.remove(element); + element.remove(); + return element; + }; + + /* Begin small 'API' */ + function onSlider(callback) { + slidercallbacks.push(callback); + } + + function getSliderPosition() { + return sliderPos; + } + + function setSliderPosition(newpos) { + newpos = Number(newpos); + if(newpos < 0 || newpos > sliderLength) return; + $("#ui-slider-handle").css('left', newpos * ($("#ui-slider-bar").width()-2) / (sliderLength * 1.0)); + $("a.tlink").map(function() { + $(this).attr('href', $(this).attr('thref').replace("%revision%", newpos)); + }); + $("#revision_label").html("Version " + newpos); + + if(newpos == 0) { + $("#leftstar").css('opacity', .5); + $("#leftstep").css('opacity', .5); + } else { + $("#leftstar").css('opacity', 1); + $("#leftstep").css('opacity', 1); + } + + if(newpos == sliderLength) { + $("#rightstar").css('opacity', .5); + $("#rightstep").css('opacity', .5); + } else { + $("#rightstar").css('opacity', 1); + $("#rightstep").css('opacity', 1); + } + + sliderPos = newpos; + _callSliderCallbacks(newpos); + } + + function getSliderLength() { + return sliderLength; + } + + function setSliderLength(newlength) { + sliderLength = newlength; + updateSliderElements(); + } + + // just take over the whole slider screen with a reconnect message + function showReconnectUI() { + if(!clientVars.sliderEnabled || !clientVars.supportsSlider) { + $("#padmain, #rightbars").css('top', "95px"); + $("#timeslider").show(); + } + $('#error').show(); + } + + function setAuthors(authors) { + $("#authorstable").empty(); + var numAnonymous = 0; + var numNamed = 0; + authors.forEach(function(author) { + if(author.name) { + numNamed ++; + var tr = $(''); + var swatchtd = $(''); + var swatch = $('
'); + swatch.css('background-color', clientVars.colorPalette[author.colorId]); + swatchtd.append(swatch); + tr.append(swatchtd); + var nametd = $(''); + nametd.text(author.name || "unnamed"); + tr.append(nametd); + $("#authorstable").append(tr); + } else { + numAnonymous ++; + } + }); + if(numAnonymous > 0) { + var html = ""+(numNamed>0?"...and ":"")+numAnonymous+" unnamed author"+(numAnonymous>1?"s":"")+""; + $("#authorstable").append($(html)); + } if(authors.length == 0) { + $("#authorstable").append($("No Authors")) + } + } + + global.BroadcastSlider = { + onSlider: onSlider, + getSliderPosition: getSliderPosition, + setSliderPosition: setSliderPosition, + getSliderLength: getSliderLength, + setSliderLength: setSliderLength, + isSliderActive: function() {return sliderActive;}, + playpause: playpause, + addSavedRevision: addSavedRevision, + showReconnectUI : showReconnectUI, + setAuthors: setAuthors + } + + function playButtonUpdater() { + if(sliderPlaying) { + if(getSliderPosition()+1 > sliderLength) { + $("#playpause_button_icon").toggleClass('pause'); + sliderPlaying = false; + return; + } + setSliderPosition(getSliderPosition()+1); + + setTimeout(playButtonUpdater, 100); + } + } + + function playpause() { + $("#playpause_button_icon").toggleClass('pause'); + + if(!sliderPlaying) { + if(getSliderPosition() == sliderLength) + setSliderPosition(0); + sliderPlaying = true; + playButtonUpdater(); + } else { + sliderPlaying = false; + } + } + + // assign event handlers to html UI elements after page load + $(window).load(function() { + disableSelection($("#playpause_button")[0]); + disableSelection($("#timeslider")[0]); + + if(clientVars.sliderEnabled && clientVars.supportsSlider) { + $(document).keyup(function(e) { + var code = -1; + if (!e) var e = window.event; + if (e.keyCode) code = e.keyCode; + else if (e.which) code = e.which; + + if(code == 37) { // left + if(!e.shiftKey) { + setSliderPosition(getSliderPosition() - 1); + } else { + var nextStar = 0; // default to first revision in document + for(var i=0; i getSliderPosition() && nextStar > pos) + nextStar = pos; + } + setSliderPosition(nextStar); + } + } else if(code == 32) + playpause(); + + }); + } + + $(window).resize(function() { + updateSliderElements(); + }); + + $("#ui-slider-bar").mousedown(function(evt) { + setSliderPosition(Math.floor((evt.clientX-$("#ui-slider-bar").offset().left) * sliderLength / 742)); + $("#ui-slider-handle").css('left', (evt.clientX-$("#ui-slider-bar").offset().left)); + $("#ui-slider-handle").trigger(evt); + }); + + // Slider dragging + $("#ui-slider-handle").mousedown(function(evt) { + this.startLoc = evt.clientX; + this.currentLoc = parseInt($(this).css('left')); + var self = this; + sliderActive = true; + $(document).mousemove(function(evt2) { + $(self).css('pointer', 'move') + var newloc = self.currentLoc + (evt2.clientX - self.startLoc); + if(newloc < 0) newloc = 0; + if(newloc > ($("#ui-slider-bar").width()-2)) newloc = ($("#ui-slider-bar").width()-2); + $("#revision_label").html("Version " + Math.floor(newloc * sliderLength / ($("#ui-slider-bar").width()-2))); + $(self).css('left', newloc); + if(getSliderPosition() != Math.floor(newloc * sliderLength / ($("#ui-slider-bar").width()-2))) + _callSliderCallbacks(Math.floor(newloc * sliderLength / ($("#ui-slider-bar").width()-2))) + }); + $(document).mouseup(function(evt2) { + $(document).unbind('mousemove'); + $(document).unbind('mouseup'); + sliderActive = false; + var newloc = self.currentLoc + (evt2.clientX - self.startLoc); + if(newloc < 0) newloc = 0; + if(newloc > ($("#ui-slider-bar").width()-2)) newloc = ($("#ui-slider-bar").width()-2); + $(self).css('left', newloc); + // if(getSliderPosition() != Math.floor(newloc * sliderLength / ($("#ui-slider-bar").width()-2))) + setSliderPosition(Math.floor(newloc * sliderLength / ($("#ui-slider-bar").width()-2))) + self.currentLoc = parseInt($(self).css('left')); + }); + }) + + // play/pause toggling + $("#playpause_button").mousedown(function(evt) { + var self = this; + + $(self).css('background-image', 'url(/static/img/pad/timeslider/crushed_button_depressed.png)'); + $(self).mouseup(function(evt2) { + $(self).css('background-image', 'url(/static/img/pad/timeslider/crushed_button_undepressed.png)'); + $(self).unbind('mouseup'); + BroadcastSlider.playpause(); + }); + $(document).mouseup(function(evt2) { + $(self).css('background-image', 'url(/static/img/pad/timeslider/crushed_button_undepressed.png)'); + $(document).unbind('mouseup'); + }); + }); + + // next/prev saved revision and changeset + $('.stepper').mousedown(function(evt) { + var self = this; + var origcss = $(self).css('background-position'); + if (! origcss) { + origcss = $(self).css('background-position-x')+" "+$(self).css('background-position-y'); + } + var origpos = parseInt(origcss.split(" ")[1]); + var newpos = (origpos - 43); + if(newpos < 0) newpos += 87; + + var newcss = (origcss.split(" ")[0] + " " + newpos + "px"); + if($(self).css('opacity') != 1.0) + newcss = origcss; + + $(self).css('background-position', newcss) + + $(self).mouseup(function(evt2) { + $(self).css('background-position',origcss); + $(self).unbind('mouseup'); + $(document).unbind('mouseup'); + if($(self).attr("id") == ("leftstep")) { + setSliderPosition(getSliderPosition() - 1); + } + else if($(self).attr("id") == ("rightstep")) { + setSliderPosition(getSliderPosition() + 1); + } + else if($(self).attr("id") == ("leftstar")) { + var nextStar = 0; // default to first revision in document + for(var i=0; i getSliderPosition() && nextStar > pos) + nextStar = pos; + } + setSliderPosition(nextStar); + } + }); + $(document).mouseup(function(evt2) { + $(self).css('background-position',origcss); + $(self).unbind('mouseup'); + $(document).unbind('mouseup'); + }); + }) + + if(clientVars) { + if(clientVars.fullWidth) { + $("#padpage").css('width', '100%'); + $("#revision").css('position', "absolute") + $("#revision").css('right', "20px") + $("#revision").css('top', "20px") + $("#padmain").css('left', '0px'); + $("#padmain").css('right', '197px'); + $("#padmain").css('width', 'auto'); + $("#rightbars").css('right', '7px'); + $("#rightbars").css('margin-right', '0px'); + $("#timeslider").css('width', 'auto'); + } + + if(clientVars.disableRightBar) { + $("#rightbars").css('display', 'none'); + $('#padmain').css('width', 'auto'); + if(clientVars.fullWidth) + $("#padmain").css('right', '7px'); + else + $("#padmain").css('width', '860px'); + $("#revision").css('position', "absolute"); + $("#revision").css('right', "20px"); + $("#revision").css('top', "20px"); + } + + + if(clientVars.sliderEnabled) { + if(clientVars.supportsSlider) { + $("#padmain, #rightbars").css('top', "95px"); + $("#timeslider").show(); + setSliderLength(clientVars.totalRevs); + setSliderPosition(clientVars.revNum); + clientVars.savedRevisions.forEach(function(revision) { + addSavedRevision(revision.revNum, revision); + }) + } else { + // slider is not supported + $("#padmain, #rightbars").css('top', "95px"); + $("#timeslider").show(); + $("#error").html("The timeslider feature is not supported on this pad. Why not?"); + $("#error").show(); + } + } else { + if(clientVars.supportsSlider) { + setSliderLength(clientVars.totalRevs); + setSliderPosition(clientVars.revNum); + } + } + } + }); +})(); + +BroadcastSlider.onSlider(function(loc) { + $("#viewlatest").html(loc==BroadcastSlider.getSliderLength()?"Viewing latest content":"View latest content"); +}) diff --git a/trunk/etherpad/src/static/js/collab_client.js b/trunk/etherpad/src/static/js/collab_client.js new file mode 100644 index 0000000..d8834d7 --- /dev/null +++ b/trunk/etherpad/src/static/js/collab_client.js @@ -0,0 +1,628 @@ +/** + * 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. + */ + +$(window).bind("load", function() { + getCollabClient.windowLoaded = true; +}); + +/** Call this when the document is ready, and a new Ace2Editor() has been created and inited. + ACE's ready callback does not need to have fired yet. + "serverVars" are from calling doc.getCollabClientVars() on the server. */ +function getCollabClient(ace2editor, serverVars, initialUserInfo, options) { + var editor = ace2editor; + + var rev = serverVars.rev; + var padId = serverVars.padId; + var globalPadId = serverVars.globalPadId; + + var state = "IDLE"; + var stateMessage; + var stateMessageSocketId; + var channelState = "CONNECTING"; + var appLevelDisconnectReason = null; + + var lastCommitTime = 0; + var initialStartConnectTime = 0; + + var userId = initialUserInfo.userId; + var socketId; + var socket; + var userSet = {}; // userId -> userInfo + userSet[userId] = initialUserInfo; + + var reconnectTimes = []; + var caughtErrors = []; + var caughtErrorCatchers = []; + var caughtErrorTimes = []; + var debugMessages = []; + + tellAceAboutHistoricalAuthors(serverVars.historicalAuthorData); + tellAceActiveAuthorInfo(initialUserInfo); + + var callbacks = { + onUserJoin: function() {}, + onUserLeave: function() {}, + onUpdateUserInfo: function() {}, + onChannelStateChange: function() {}, + onClientMessage: function() {}, + onInternalAction: function() {}, + onConnectionTrouble: function() {}, + onServerMessage: function() {} + }; + + $(window).bind("unload", function() { + if (socket) { + socket.onclosed = function() {}; + socket.onhiccup = function() {}; + socket.disconnect(true); + } + }); + if ($.browser.mozilla) { + // Prevent "escape" from taking effect and canceling a comet connection; + // doesn't work if focus is on an iframe. + $(window).bind("keydown", function(evt) { if (evt.which == 27) { evt.preventDefault() } }); + } + + editor.setProperty("userAuthor", userId); + editor.setBaseAttributedText(serverVars.initialAttributedText, serverVars.apool); + editor.setUserChangeNotificationCallback(wrapRecordingErrors("handleUserChanges", handleUserChanges)); + + function abandonConnection(reason) { + if (socket) { + socket.onclosed = function() {}; + socket.onhiccup = function() {}; + socket.disconnect(); + } + socket = null; + setChannelState("DISCONNECTED", reason); + } + + function dmesg(str) { + if (typeof window.ajlog == "string") window.ajlog += str+'\n'; + debugMessages.push(str); + } + + function handleUserChanges() { + if ((! socket) || channelState == "CONNECTING") { + if (channelState == "CONNECTING" && (((+new Date()) - initialStartConnectTime) > 20000)) { + abandonConnection("initsocketfail"); // give up + } + else { + // check again in a bit + setTimeout(wrapRecordingErrors("setTimeout(handleUserChanges)", handleUserChanges), + 1000); + } + return; + } + + var t = (+new Date()); + + if (state != "IDLE") { + if (state == "COMMITTING" && (t - lastCommitTime) > 20000) { + // a commit is taking too long + appLevelDisconnectReason = "slowcommit"; + socket.disconnect(); + } + else if (state == "COMMITTING" && (t - lastCommitTime) > 5000) { + callbacks.onConnectionTrouble("SLOW"); + } + else { + // run again in a few seconds, to detect a disconnect + setTimeout(wrapRecordingErrors("setTimeout(handleUserChanges)", handleUserChanges), + 3000); + } + return; + } + + var earliestCommit = lastCommitTime + 500; + if (t < earliestCommit) { + setTimeout(wrapRecordingErrors("setTimeout(handleUserChanges)", handleUserChanges), + earliestCommit - t); + return; + } + + var sentMessage = false; + var userChangesData = editor.prepareUserChangeset(); + if (userChangesData.changeset) { + lastCommitTime = t; + state = "COMMITTING"; + stateMessage = {type:"USER_CHANGES", baseRev:rev, + changeset:userChangesData.changeset, + apool: userChangesData.apool }; + stateMessageSocketId = socketId; + sendMessage(stateMessage); + sentMessage = true; + callbacks.onInternalAction("commitPerformed"); + } + + if (sentMessage) { + // run again in a few seconds, to detect a disconnect + setTimeout(wrapRecordingErrors("setTimeout(handleUserChanges)", handleUserChanges), + 3000); + } + } + + function getStats() { + var stats = {}; + + stats.screen = [$(window).width(), $(window).height(), + window.screen.availWidth, window.screen.availHeight, + window.screen.width, window.screen.height].join(','); + stats.ip = serverVars.clientIp; + stats.useragent = serverVars.clientAgent; + + return stats; + } + + function setUpSocket() { + var success = false; + callCatchingErrors("setUpSocket", function() { + appLevelDisconnectReason = null; + + var oldSocketId = socketId; + socketId = String(Math.floor(Math.random()*1e12)); + socket = new WebSocket(socketId); + socket.onmessage = wrapRecordingErrors("socket.onmessage", handleMessageFromServer); + socket.onclosed = wrapRecordingErrors("socket.onclosed", handleSocketClosed); + socket.onopen = wrapRecordingErrors("socket.onopen", function() { + hiccupCount = 0; + setChannelState("CONNECTED"); + var msg = { type:"CLIENT_READY", roomType:'padpage', + roomName:'padpage/'+globalPadId, + data: { + lastRev:rev, + userInfo:userSet[userId], + stats: getStats() } }; + if (oldSocketId) { + msg.data.isReconnectOf = oldSocketId; + msg.data.isCommitPending = (state == "COMMITTING"); + } + sendMessage(msg); + doDeferredActions(); + }); + socket.onhiccup = wrapRecordingErrors("socket.onhiccup", handleCometHiccup); + socket.onlogmessage = dmesg; + socket.connect(); + success = true; + }); + if (success) { + initialStartConnectTime = +new Date(); + } + else { + abandonConnection("initsocketfail"); + } + } + function setUpSocketWhenWindowLoaded() { + if (getCollabClient.windowLoaded) { + setUpSocket(); + } + else { + setTimeout(setUpSocketWhenWindowLoaded, 200); + } + } + setTimeout(setUpSocketWhenWindowLoaded, 0); + + var hiccupCount = 0; + function handleCometHiccup(params) { + dmesg("HICCUP (connected:"+(!!params.connected)+")"); + var connectedNow = params.connected; + if (! connectedNow) { + hiccupCount++; + // skip first "cut off from server" notification + if (hiccupCount > 1) { + setChannelState("RECONNECTING"); + } + } + else { + hiccupCount = 0; + setChannelState("CONNECTED"); + } + } + + function sendMessage(msg) { + socket.postMessage(JSON.stringify({type: "COLLABROOM", data: msg})); + } + + function wrapRecordingErrors(catcher, func) { + return function() { + try { + return func.apply(this, Array.prototype.slice.call(arguments)); + } + catch (e) { + caughtErrors.push(e); + caughtErrorCatchers.push(catcher); + caughtErrorTimes.push(+new Date()); + //console.dir({catcher: catcher, e: e}); + throw e; + } + }; + } + + function callCatchingErrors(catcher, func) { + try { + wrapRecordingErrors(catcher, func)(); + } + catch (e) { /*absorb*/ } + } + + function handleMessageFromServer(evt) { + if (! socket) return; + if (! evt.data) return; + var wrapper = JSON.parse(evt.data); + if(wrapper.type != "COLLABROOM") return; + var msg = wrapper.data; + if (msg.type == "NEW_CHANGES") { + var newRev = msg.newRev; + var changeset = msg.changeset; + var author = (msg.author || ''); + var apool = msg.apool; + if (newRev != (rev+1)) { + dmesg("bad message revision on NEW_CHANGES: "+newRev+" not "+(rev+1)); + socket.disconnect(); + return; + } + rev = newRev; + editor.applyChangesToBase(changeset, author, apool); + } + else if (msg.type == "ACCEPT_COMMIT") { + var newRev = msg.newRev; + if (newRev != (rev+1)) { + dmesg("bad message revision on ACCEPT_COMMIT: "+newRev+" not "+(rev+1)); + socket.disconnect(); + return; + } + rev = newRev; + editor.applyPreparedChangesetToBase(); + setStateIdle(); + callCatchingErrors("onInternalAction", function() { + callbacks.onInternalAction("commitAcceptedByServer"); + }); + callCatchingErrors("onConnectionTrouble", function() { + callbacks.onConnectionTrouble("OK"); + }); + handleUserChanges(); + } + else if (msg.type == "NO_COMMIT_PENDING") { + if (state == "COMMITTING") { + // server missed our commit message; abort that commit + setStateIdle(); + handleUserChanges(); + } + } + else if (msg.type == "USER_NEWINFO") { + var userInfo = msg.userInfo; + var id = userInfo.userId; + if (userSet[id]) { + userSet[id] = userInfo; + callbacks.onUpdateUserInfo(userInfo); + dmesgUsers(); + } + else { + userSet[id] = userInfo; + callbacks.onUserJoin(userInfo); + dmesgUsers(); + } + tellAceActiveAuthorInfo(userInfo); + } + else if (msg.type == "USER_LEAVE") { + var userInfo = msg.userInfo; + var id = userInfo.userId; + if (userSet[id]) { + delete userSet[userInfo.userId]; + fadeAceAuthorInfo(userInfo); + callbacks.onUserLeave(userInfo); + dmesgUsers(); + } + } + else if (msg.type == "DISCONNECT_REASON") { + appLevelDisconnectReason = msg.reason; + } + else if (msg.type == "CLIENT_MESSAGE") { + callbacks.onClientMessage(msg.payload); + } + else if (msg.type == "SERVER_MESSAGE") { + callbacks.onServerMessage(msg.payload); + } + } + function updateUserInfo(userInfo) { + userInfo.userId = userId; + userSet[userId] = userInfo; + tellAceActiveAuthorInfo(userInfo); + if (! socket) return; + sendMessage({type: "USERINFO_UPDATE", userInfo:userInfo}); + } + + function tellAceActiveAuthorInfo(userInfo) { + tellAceAuthorInfo(userInfo.userId, userInfo.colorId); + } + function tellAceAuthorInfo(userId, colorId, inactive) { + if (colorId || (typeof colorId) == "number") { + colorId = Number(colorId); + if (options && options.colorPalette && options.colorPalette[colorId]) { + var cssColor = options.colorPalette[colorId]; + if (inactive) { + editor.setAuthorInfo(userId, {bgcolor: cssColor, fade: 0.5}); + } + else { + editor.setAuthorInfo(userId, {bgcolor: cssColor}); + } + } + } + } + function fadeAceAuthorInfo(userInfo) { + tellAceAuthorInfo(userInfo.userId, userInfo.colorId, true); + } + + function getConnectedUsers() { + return valuesArray(userSet); + } + + function tellAceAboutHistoricalAuthors(hadata) { + for(var author in hadata) { + var data = hadata[author]; + if (! userSet[author]) { + tellAceAuthorInfo(author, data.colorId, true); + } + } + } + + function dmesgUsers() { + //pad.dmesg($.map(getConnectedUsers(), function(u) { return u.userId.slice(-2); }).join(',')); + } + + function handleSocketClosed(params) { + socket = null; + + $.each(keys(userSet), function() { + var uid = String(this); + if (uid != userId) { + var userInfo = userSet[uid]; + delete userSet[uid]; + callbacks.onUserLeave(userInfo); + dmesgUsers(); + } + }); + + var reason = appLevelDisconnectReason || params.reason; + var shouldReconnect = params.reconnect; + if (shouldReconnect) { + + // determine if this is a tight reconnect loop due to weird connectivity problems + reconnectTimes.push(+new Date()); + var TOO_MANY_RECONNECTS = 8; + var TOO_SHORT_A_TIME_MS = 10000; + if (reconnectTimes.length >= TOO_MANY_RECONNECTS && + ((+new Date()) - reconnectTimes[reconnectTimes.length-TOO_MANY_RECONNECTS]) < + TOO_SHORT_A_TIME_MS) { + setChannelState("DISCONNECTED", "looping"); + } + else { + setChannelState("RECONNECTING", reason); + setUpSocket(); + } + + } + else { + setChannelState("DISCONNECTED", reason); + } + } + + function setChannelState(newChannelState, moreInfo) { + if (newChannelState != channelState) { + channelState = newChannelState; + callbacks.onChannelStateChange(channelState, moreInfo); + } + } + + function keys(obj) { + var array = []; + $.each(obj, function (k, v) { array.push(k); }); + return array; + } + function valuesArray(obj) { + var array = []; + $.each(obj, function (k, v) { array.push(v); }); + return array; + } + + // We need to present a working interface even before the socket + // is connected for the first time. + var deferredActions = []; + function defer(func, tag) { + return function() { + var that = this; + var args = arguments; + function action() { + func.apply(that, args); + } + action.tag = tag; + if (channelState == "CONNECTING") { + deferredActions.push(action); + } + else { + action(); + } + } + } + function doDeferredActions(tag) { + var newArray = []; + for(var i=0;i maxDebugMessages) { + debugMessages = debugMessages.slice(debugMessages.length-maxDebugMessages, + debugMessages.length); + } + + info.debugMessages = {length: 0}; + for(var i=0;i 0) { + var f = idleFuncs.shift(); + f(); + } + } + }, 0); + } + + var self; + return (self = { + setOnUserJoin: function(cb) { callbacks.onUserJoin = cb; }, + setOnUserLeave: function(cb) { callbacks.onUserLeave = cb; }, + setOnUpdateUserInfo: function(cb) { callbacks.onUpdateUserInfo = cb; }, + setOnChannelStateChange: function(cb) { callbacks.onChannelStateChange = cb; }, + setOnClientMessage: function(cb) { callbacks.onClientMessage = cb; }, + setOnInternalAction: function(cb) { callbacks.onInternalAction = cb; }, + setOnConnectionTrouble: function(cb) { callbacks.onConnectionTrouble = cb; }, + setOnServerMessage: function(cb) { callbacks.onServerMessage = cb; }, + updateUserInfo: defer(updateUserInfo), + getConnectedUsers: getConnectedUsers, + sendClientMessage: sendClientMessage, + getCurrentRevisionNumber: getCurrentRevisionNumber, + getDiagnosticInfo: getDiagnosticInfo, + getMissedChanges: getMissedChanges, + callWhenNotCommitting: callWhenNotCommitting, + addHistoricalAuthors: tellAceAboutHistoricalAuthors + }); +} + +function selectElementContents(elem) { + if ($.browser.msie) { + var range = document.body.createTextRange(); + range.moveToElementText(elem); + range.select(); + } + else { + if (window.getSelection) { + var browserSelection = window.getSelection(); + if (browserSelection) { + var range = document.createRange(); + range.selectNodeContents(elem); + browserSelection.removeAllRanges(); + browserSelection.addRange(range); + } + } + } +} diff --git a/trunk/etherpad/src/static/js/colorutils.js b/trunk/etherpad/src/static/js/colorutils.js new file mode 100644 index 0000000..e745f8e --- /dev/null +++ b/trunk/etherpad/src/static/js/colorutils.js @@ -0,0 +1,91 @@ +// DO NOT EDIT THIS FILE, edit infrastructure/ace/www/colorutils.js + +/** + * 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. + */ + +var colorutils = {}; + +// "#ffffff" or "#fff" or "ffffff" or "fff" to [1.0, 1.0, 1.0] +colorutils.css2triple = function(cssColor) { + var sixHex = colorutils.css2sixhex(cssColor); + function hexToFloat(hh) { + return Number("0x"+hh)/255; + } + return [hexToFloat(sixHex.substr(0,2)), + hexToFloat(sixHex.substr(2,2)), + hexToFloat(sixHex.substr(4,2))]; +} + +// "#ffffff" or "#fff" or "ffffff" or "fff" to "ffffff" +colorutils.css2sixhex = function(cssColor) { + var h = /[0-9a-fA-F]+/.exec(cssColor)[0]; + if (h.length != 6) { + var a = h.charAt(0); + var b = h.charAt(1); + var c = h.charAt(2); + h = a+a+b+b+c+c; + } + return h; +} + +// [1.0, 1.0, 1.0] -> "#ffffff" +colorutils.triple2css = function(triple) { + function floatToHex(n) { + var n2 = colorutils.clamp(Math.round(n*255), 0, 255); + return ("0"+n2.toString(16)).slice(-2); + } + return "#" + floatToHex(triple[0]) + + floatToHex(triple[1]) + floatToHex(triple[2]); +} + + +colorutils.clamp = function(v,bot,top) { return v < bot ? bot : (v > top ? top : v); }; +colorutils.min3 = function(a,b,c) { return (a < b) ? (a < c ? a : c) : (b < c ? b : c); }; +colorutils.max3 = function(a,b,c) { return (a > b) ? (a > c ? a : c) : (b > c ? b : c); }; +colorutils.colorMin = function(c) { return colorutils.min3(c[0], c[1], c[2]); }; +colorutils.colorMax = function(c) { return colorutils.max3(c[0], c[1], c[2]); }; +colorutils.scale = function(v, bot, top) { return colorutils.clamp(bot + v*(top-bot), 0, 1); }; +colorutils.unscale = function(v, bot, top) { return colorutils.clamp((v-bot)/(top-bot), 0, 1); }; + +colorutils.scaleColor = function(c, bot, top) { + return [colorutils.scale(c[0], bot, top), + colorutils.scale(c[1], bot, top), + colorutils.scale(c[2], bot, top)]; +} + +colorutils.unscaleColor = function(c, bot, top) { + return [colorutils.unscale(c[0], bot, top), + colorutils.unscale(c[1], bot, top), + colorutils.unscale(c[2], bot, top)]; +} + +colorutils.luminosity = function(c) { + // rule of thumb for RGB brightness; 1.0 is white + return c[0]*0.30 + c[1]*0.59 + c[2]*0.11; +} + +colorutils.saturate = function(c) { + var min = colorutils.colorMin(c); + var max = colorutils.colorMax(c); + if (max - min <= 0) return [1.0, 1.0, 1.0]; + return colorutils.unscaleColor(c, min, max); +} + +colorutils.blend = function(c1, c2, t) { + return [colorutils.scale(t, c1[0], c2[0]), + colorutils.scale(t, c1[1], c2[1]), + colorutils.scale(t, c1[2], c2[2])]; +} diff --git a/trunk/etherpad/src/static/js/confirmation.js b/trunk/etherpad/src/static/js/confirmation.js new file mode 100644 index 0000000..a0f725c --- /dev/null +++ b/trunk/etherpad/src/static/js/confirmation.js @@ -0,0 +1,21 @@ +/** + * 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() { + $('#shoppingform').submit(function() { + $('#contbutton').attr("disabled", true).attr("value", "Purchasing..."); + }); +}) \ No newline at end of file diff --git a/trunk/etherpad/src/static/js/connection_diagnostics.js b/trunk/etherpad/src/static/js/connection_diagnostics.js new file mode 100644 index 0000000..cc43d46 --- /dev/null +++ b/trunk/etherpad/src/static/js/connection_diagnostics.js @@ -0,0 +1,126 @@ +/** + * 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. + */ + +diagnostics = {}; + +diagnostics.data = {}; + +diagnostics.steps = [ + ['init', "Initializing"], + ['examineBrowser', "Examining web browser"], + ['testStreaming', "Testing primary transport (streaming)"], + ['testPolling', "Testing secondary transport (polling)"], + ['testHiccups', "Testing connection hiccups"], + ['sendInfo', "Sending information"], + ['showResult', ""] +]; + +diagnostics.processNext = function(i) { + if (i < diagnostics.steps.length) { + var msg = "Step "+(i+1)+": "+diagnostics.steps[i][1]+"..."; + $('#statusmsg').html(msg); + diagnostics[diagnostics.steps[i][0]](function() { + diagnostics.processNext(i+1); + }); + } +}; + +$(document).ready(function() { + diagnostics.processNext(0); + + var emailClicked = false; + $('#email').click(function() { + if (!emailClicked) { + $('#email').select(); + emailClicked = true; + } + }); + + $('#emailsubmit').click(function() { + function err(m) { + $('#emailerrormsg').hide().html(m).fadeIn('fast'); + } + var email = $('#email').val(); + if (!etherpad.validEmail(email)) { + err("That doesn't look like a valid email address."); + return; + } + $.ajax({ + type: 'post', + url: '/ep/connection-diagnostics/submitemail', + data: {email: email, diagnosticStorableId: clientVars.diagnosticStorableId}, + success: success, + error: error + }); + function success(responseText) { + if (responseText == "OK") { + $('#emailform').html("

Thanks! We will look at your case shortly.

"); + } else { + err(responseText); + } + } + function error() { + err("There was an error processing your request."); + } + }); +}); + +diagnostics.init = function(done) { + setTimeout(done, 1000); +}; + +diagnostics.examineBrowser = function(done) { + setTimeout(done, 1000); +}; + +diagnostics.testStreaming = function(done) { + setTimeout(done, 1000); +}; + +diagnostics.testPolling = function(done) { + setTimeout(done, 1000); +}; + +diagnostics.testHiccups = function(done) { + setTimeout(done, 1000); +}; + +diagnostics.sendInfo = function(done) { + + // TODO(jd): remove these test data when you submit actual data. + diagnostics.data.test1 = "foo"; + diagnostics.data.test2 = "bar"; + diagnostics.data.testNested = {a: 1, b: 2, c: 3}; + + // send data object back to server. + $.ajax({ + type: 'post', + url: '/ep/connection-diagnostics/submitdata', + data: {dataJson: JSON.stringify(diagnostics.data), + diagnosticStorableId: clientVars.diagnosticStorableId}, + success: done, + error: function() { alert("There was an error submitting the diagnostic information to the server."); done(); } + }); +}; + +diagnostics.showResult = function(done) { + $('#linkanimation').hide(); + $('#statusmsg').html("
Result: your browser and internet" + + " connection appear to be incompatibile with EtherPad."); + $('#statusmsg').css('color', '#520'); + $('#emailform').show(); +}; + diff --git a/trunk/etherpad/src/static/js/cssmanager_client.js b/trunk/etherpad/src/static/js/cssmanager_client.js new file mode 100644 index 0000000..04ed641 --- /dev/null +++ b/trunk/etherpad/src/static/js/cssmanager_client.js @@ -0,0 +1,88 @@ +// DO NOT EDIT THIS FILE, edit infrastructure/ace/www/cssmanager.js + +/** + * 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 makeCSSManager(emptyStylesheetTitle) { + + function getSheetByTitle(title) { + var allSheets = document.styleSheets; + for(var i=0;i= 0) { + browserDeleteRule(i); + selectorList.splice(i, 1); + } + } + + return {selectorStyle:selectorStyle, removeSelectorStyle:removeSelectorStyle, + info: function() { + return selectorList.length+":"+browserRules().length; + }}; +} diff --git a/trunk/etherpad/src/static/js/domline_client.js b/trunk/etherpad/src/static/js/domline_client.js new file mode 100644 index 0000000..de2e7d3 --- /dev/null +++ b/trunk/etherpad/src/static/js/domline_client.js @@ -0,0 +1,210 @@ +// DO NOT EDIT THIS FILE, edit infrastructure/ace/www/domline.js + +/** + * 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. + */ + +var domline = {}; +domline.noop = function() {}; +domline.identity = function(x) { return x; }; + +domline.addToLineClass = function(lineClass, cls) { + // an "empty span" at any point can be used to add classes to + // the line, using line:className. otherwise, we ignore + // the span. + cls.replace(/\S+/g, function (c) { + if (c.indexOf("line:") == 0) { + // add class to line + lineClass = (lineClass ? lineClass+' ' : '')+c.substring(5); + } + }); + return lineClass; +} + +// if "document" is falsy we don't create a DOM node, just +// an object with innerHTML and className +domline.createDomLine = function(nonEmpty, doesWrap, optBrowser, optDocument) { + var result = { node: null, + appendSpan: domline.noop, + prepareForAdd: domline.noop, + notifyAdded: domline.noop, + clearSpans: domline.noop, + finishUpdate: domline.noop, + lineMarker: 0 }; + + var browser = (optBrowser || {}); + var document = optDocument; + + if (document) { + result.node = document.createElement("div"); + } + else { + result.node = {innerHTML: '', className: ''}; + } + + var html = []; + var preHtml, postHtml; + var curHTML = null; + function processSpaces(s) { + return domline.processSpaces(s, doesWrap); + } + var identity = domline.identity; + var perTextNodeProcess = (doesWrap ? identity : processSpaces); + var perHtmlLineProcess = (doesWrap ? processSpaces : identity); + var lineClass = 'ace-line'; + result.appendSpan = function(txt, cls) { + if (cls.indexOf('list') >= 0) { + var listType = /(?:^| )list:(\S+)/.exec(cls); + if (listType) { + listType = listType[1]; + if (listType) { + preHtml = '
  • '; + postHtml = '
'; + } + result.lineMarker += txt.length; + return; // don't append any text + } + } + var href = null; + var simpleTags = null; + if (cls.indexOf('url') >= 0) { + cls = cls.replace(/(^| )url:(\S+)/g, function(x0, space, url) { + href = url; + return space+"url"; + }); + } + if (cls.indexOf('tag') >= 0) { + cls = cls.replace(/(^| )tag:(\S+)/g, function(x0, space, tag) { + if (! simpleTags) simpleTags = []; + simpleTags.push(tag.toLowerCase()); + return space+tag; + }); + } + if ((! txt) && cls) { + lineClass = domline.addToLineClass(lineClass, cls); + } + else if (txt) { + var extraOpenTags = ""; + var extraCloseTags = ""; + if (href) { + extraOpenTags = extraOpenTags+''; + extraCloseTags = ''+extraCloseTags; + } + if (simpleTags) { + simpleTags.sort(); + extraOpenTags = extraOpenTags+'<'+simpleTags.join('><')+'>'; + simpleTags.reverse(); + extraCloseTags = ''+extraCloseTags; + } + html.push('',extraOpenTags, + perTextNodeProcess(domline.escapeHTML(txt)), + extraCloseTags,''); + } + }; + result.clearSpans = function() { + html = []; + lineClass = ''; // non-null to cause update + result.lineMarker = 0; + }; + function writeHTML() { + var newHTML = perHtmlLineProcess(html.join('')); + if (! newHTML) { + if ((! document) || (! optBrowser)) { + newHTML += ' '; + } + else if (! browser.msie) { + newHTML += '
'; + } + } + if (nonEmpty) { + newHTML = (preHtml||'')+newHTML+(postHtml||''); + } + html = preHtml = postHtml = null; // free memory + if (newHTML !== curHTML) { + curHTML = newHTML; + result.node.innerHTML = curHTML; + } + if (lineClass !== null) result.node.className = lineClass; + } + result.prepareForAdd = writeHTML; + result.finishUpdate = writeHTML; + result.getInnerHTML = function() { return curHTML || ''; }; + + return result; +}; + +domline.escapeHTML = function(s) { + var re = /[&<>'"]/g; /']/; // stupid indentation thing + if (! re.MAP) { + // persisted across function calls! + re.MAP = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + } + return s.replace(re, function(c) { return re.MAP[c]; }); +}; + +domline.processSpaces = function(s, doesWrap) { + if (s.indexOf("<") < 0 && ! doesWrap) { + // short-cut + return s.replace(/ /g, ' '); + } + var parts = []; + s.replace(/<[^>]*>?| |[^ <]+/g, function(m) { parts.push(m); }); + if (doesWrap) { + var endOfLine = true; + var beforeSpace = false; + // last space in a run is normal, others are nbsp, + // end of line is nbsp + for(var i=parts.length-1;i>=0;i--) { + var p = parts[i]; + if (p == " ") { + if (endOfLine || beforeSpace) + parts[i] = ' '; + endOfLine = false; + beforeSpace = true; + } + else if (p.charAt(0) != "<") { + endOfLine = false; + beforeSpace = false; + } + } + // beginning of line is nbsp + for(var i=0;i= ",oldLen," in ",cs); break; + case '+': { + calcNewLen += o.chars; numInserted += o.chars; + Changeset.assert(calcNewLen < newLen, calcNewLen," >= ",newLen," in ",cs); + break; + } + } + assem.append(o); + } + + calcNewLen += oldLen - oldPos; + charBank = charBank.substring(0, numInserted); + while (charBank.length < numInserted) { + charBank += "?"; + } + + assem.endDocument(); + var normalized = Changeset.pack(oldLen, calcNewLen, assem.toString(), charBank); + Changeset.assert(normalized == cs, normalized,' != ',cs); + + return cs; +} + +Changeset.smartOpAssembler = function() { + // Like opAssembler but able to produce conforming changesets + // from slightly looser input, at the cost of speed. + // Specifically: + // - merges consecutive operations that can be merged + // - strips final "=" + // - ignores 0-length changes + // - reorders consecutive + and - (which margingOpAssembler doesn't do) + + var minusAssem = Changeset.mergingOpAssembler(); + var plusAssem = Changeset.mergingOpAssembler(); + var keepAssem = Changeset.mergingOpAssembler(); + var assem = Changeset.stringAssembler(); + var lastOpcode = ''; + var lengthChange = 0; + + function flushKeeps() { + assem.append(keepAssem.toString()); + keepAssem.clear(); + } + + function flushPlusMinus() { + assem.append(minusAssem.toString()); + minusAssem.clear(); + assem.append(plusAssem.toString()); + plusAssem.clear(); + } + + function append(op) { + if (! op.opcode) return; + if (! op.chars) return; + + if (op.opcode == '-') { + if (lastOpcode == '=') { + flushKeeps(); + } + minusAssem.append(op); + lengthChange -= op.chars; + } + else if (op.opcode == '+') { + if (lastOpcode == '=') { + flushKeeps(); + } + plusAssem.append(op); + lengthChange += op.chars; + } + else if (op.opcode == '=') { + if (lastOpcode != '=') { + flushPlusMinus(); + } + keepAssem.append(op); + } + lastOpcode = op.opcode; + } + + function appendOpWithText(opcode, text, attribs, pool) { + var op = Changeset.newOp(opcode); + op.attribs = Changeset.makeAttribsString(opcode, attribs, pool); + var lastNewlinePos = text.lastIndexOf('\n'); + if (lastNewlinePos < 0) { + op.chars = text.length; + op.lines = 0; + append(op); + } + else { + op.chars = lastNewlinePos+1; + op.lines = text.match(/\n/g).length; + append(op); + op.chars = text.length - (lastNewlinePos+1); + op.lines = 0; + append(op); + } + } + + function toString() { + flushPlusMinus(); + flushKeeps(); + return assem.toString(); + } + + function clear() { + minusAssem.clear(); + plusAssem.clear(); + keepAssem.clear(); + assem.clear(); + lengthChange = 0; + } + + function endDocument() { + keepAssem.endDocument(); + } + + function getLengthChange() { + return lengthChange; + } + + return {append: append, toString: toString, clear: clear, endDocument: endDocument, + appendOpWithText: appendOpWithText, getLengthChange: getLengthChange }; +}; + +if (_opt) { + Changeset.mergingOpAssembler = function() { + var assem = _opt.mergingOpAssembler(); + + function append(op) { + assem.append(op.opcode, op.chars, op.lines, op.attribs); + } + function toString() { + return assem.toString(); + } + function clear() { + assem.clear(); + } + function endDocument() { + assem.endDocument(); + } + + return {append: append, toString: toString, clear: clear, endDocument: endDocument}; + }; +} +else { + Changeset.mergingOpAssembler = function() { + // This assembler can be used in production; it efficiently + // merges consecutive operations that are mergeable, ignores + // no-ops, and drops final pure "keeps". It does not re-order + // operations. + var assem = Changeset.opAssembler(); + var bufOp = Changeset.newOp(); + + // If we get, for example, insertions [xxx\n,yyy], those don't merge, + // but if we get [xxx\n,yyy,zzz\n], that merges to [xxx\nyyyzzz\n]. + // This variable stores the length of yyy and any other newline-less + // ops immediately after it. + var bufOpAdditionalCharsAfterNewline = 0; + + function flush(isEndDocument) { + if (bufOp.opcode) { + if (isEndDocument && bufOp.opcode == '=' && ! bufOp.attribs) { + // final merged keep, leave it implicit + } + else { + assem.append(bufOp); + if (bufOpAdditionalCharsAfterNewline) { + bufOp.chars = bufOpAdditionalCharsAfterNewline; + bufOp.lines = 0; + assem.append(bufOp); + bufOpAdditionalCharsAfterNewline = 0; + } + } + bufOp.opcode = ''; + } + } + function append(op) { + if (op.chars > 0) { + if (bufOp.opcode == op.opcode && bufOp.attribs == op.attribs) { + if (op.lines > 0) { + // bufOp and additional chars are all mergeable into a multi-line op + bufOp.chars += bufOpAdditionalCharsAfterNewline + op.chars; + bufOp.lines += op.lines; + bufOpAdditionalCharsAfterNewline = 0; + } + else if (bufOp.lines == 0) { + // both bufOp and op are in-line + bufOp.chars += op.chars; + } + else { + // append in-line text to multi-line bufOp + bufOpAdditionalCharsAfterNewline += op.chars; + } + } + else { + flush(); + Changeset.copyOp(op, bufOp); + } + } + } + function endDocument() { + flush(true); + } + function toString() { + flush(); + return assem.toString(); + } + function clear() { + assem.clear(); + Changeset.clearOp(bufOp); + } + return {append: append, toString: toString, clear: clear, endDocument: endDocument}; + }; +} + +if (_opt) { + Changeset.opAssembler = function() { + var assem = _opt.opAssembler(); + // this function allows op to be mutated later (doesn't keep a ref) + function append(op) { + assem.append(op.opcode, op.chars, op.lines, op.attribs); + } + function toString() { + return assem.toString(); + } + function clear() { + assem.clear(); + } + return {append: append, toString: toString, clear: clear}; + }; +} +else { + Changeset.opAssembler = function() { + var pieces = []; + // this function allows op to be mutated later (doesn't keep a ref) + function append(op) { + pieces.push(op.attribs); + if (op.lines) { + pieces.push('|', Changeset.numToString(op.lines)); + } + pieces.push(op.opcode); + pieces.push(Changeset.numToString(op.chars)); + } + function toString() { + return pieces.join(''); + } + function clear() { + pieces.length = 0; + } + return {append: append, toString: toString, clear: clear}; + }; +} + +Changeset.stringIterator = function(str) { + var curIndex = 0; + function assertRemaining(n) { + Changeset.assert(n <= remaining(), "!(",n," <= ",remaining(),")"); + } + function take(n) { + assertRemaining(n); + var s = str.substr(curIndex, n); + curIndex += n; + return s; + } + function peek(n) { + assertRemaining(n); + var s = str.substr(curIndex, n); + return s; + } + function skip(n) { + assertRemaining(n); + curIndex += n; + } + function remaining() { + return str.length - curIndex; + } + return {take:take, skip:skip, remaining:remaining, peek:peek}; +}; + +Changeset.stringAssembler = function() { + var pieces = []; + function append(x) { + pieces.push(String(x)); + } + function toString() { + return pieces.join(''); + } + return {append: append, toString: toString}; +}; + +// "lines" need not be an array as long as it supports certain calls (lines_foo inside). +Changeset.textLinesMutator = function(lines) { + // Mutates lines, an array of strings, in place. + // Mutation operations have the same constraints as changeset operations + // with respect to newlines, but not the other additional constraints + // (i.e. ins/del ordering, forbidden no-ops, non-mergeability, final newline). + // Can be used to mutate lists of strings where the last char of each string + // is not actually a newline, but for the purposes of N and L values, + // the caller should pretend it is, and for things to work right in that case, the input + // to insert() should be a single line with no newlines. + + var curSplice = [0,0]; + var inSplice = false; + // position in document after curSplice is applied: + var curLine = 0, curCol = 0; + // invariant: if (inSplice) then (curLine is in curSplice[0] + curSplice.length - {2,3}) && + // curLine >= curSplice[0] + // invariant: if (inSplice && (curLine >= curSplice[0] + curSplice.length - 2)) then + // curCol == 0 + + function lines_applySplice(s) { + lines.splice.apply(lines, s); + } + function lines_toSource() { + return lines.toSource(); + } + function lines_get(idx) { + if (lines.get) { + return lines.get(idx); + } + else { + return lines[idx]; + } + } + // can be unimplemented if removeLines's return value not needed + function lines_slice(start, end) { + if (lines.slice) { + return lines.slice(start, end); + } + else { + return []; + } + } + function lines_length() { + if ((typeof lines.length) == "number") { + return lines.length; + } + else { + return lines.length(); + } + } + + function enterSplice() { + curSplice[0] = curLine; + curSplice[1] = 0; + if (curCol > 0) { + putCurLineInSplice(); + } + inSplice = true; + } + function leaveSplice() { + lines_applySplice(curSplice); + curSplice.length = 2; + curSplice[0] = curSplice[1] = 0; + inSplice = false; + } + function isCurLineInSplice() { + return (curLine - curSplice[0] < (curSplice.length - 2)); + } + function debugPrint(typ) { + print(typ+": "+curSplice.toSource()+" / "+curLine+","+curCol+" / "+lines_toSource()); + } + function putCurLineInSplice() { + if (! isCurLineInSplice()) { + curSplice.push(lines_get(curSplice[0] + curSplice[1])); + curSplice[1]++; + } + return 2 + curLine - curSplice[0]; + } + + function skipLines(L, includeInSplice) { + if (L) { + if (includeInSplice) { + if (! inSplice) { + enterSplice(); + } + for(var i=0;i 1) { + leaveSplice(); + } + else { + putCurLineInSplice(); + } + } + curLine += L; + curCol = 0; + } + //print(inSplice+" / "+isCurLineInSplice()+" / "+curSplice[0]+" / "+curSplice[1]+" / "+lines.length); + /*if (inSplice && (! isCurLineInSplice()) && (curSplice[0] + curSplice[1] < lines.length)) { + print("BLAH"); + putCurLineInSplice(); + }*/ // tests case foo in remove(), which isn't otherwise covered in current impl + } + //debugPrint("skip"); + } + + function skip(N, L, includeInSplice) { + if (N) { + if (L) { + skipLines(L, includeInSplice); + } + else { + if (includeInSplice && ! inSplice) { + enterSplice(); + } + if (inSplice) { + putCurLineInSplice(); + } + curCol += N; + //debugPrint("skip"); + } + } + } + + function removeLines(L) { + var removed = ''; + if (L) { + if (! inSplice) { + enterSplice(); + } + function nextKLinesText(k) { + var m = curSplice[0] + curSplice[1]; + return lines_slice(m, m+k).join(''); + } + if (isCurLineInSplice()) { + //print(curCol); + if (curCol == 0) { + removed = curSplice[curSplice.length-1]; + // print("FOO"); // case foo + curSplice.length--; + removed += nextKLinesText(L-1); + curSplice[1] += L-1; + } + else { + removed = nextKLinesText(L-1); + curSplice[1] += L-1; + var sline = curSplice.length - 1; + removed = curSplice[sline].substring(curCol) + removed; + curSplice[sline] = curSplice[sline].substring(0, curCol) + + lines_get(curSplice[0] + curSplice[1]); + curSplice[1] += 1; + } + } + else { + removed = nextKLinesText(L); + curSplice[1] += L; + } + //debugPrint("remove"); + } + return removed; + } + + function remove(N, L) { + var removed = ''; + if (N) { + if (L) { + return removeLines(L); + } + else { + if (! inSplice) { + enterSplice(); + } + var sline = putCurLineInSplice(); + removed = curSplice[sline].substring(curCol, curCol+N); + curSplice[sline] = curSplice[sline].substring(0, curCol) + + curSplice[sline].substring(curCol+N); + //debugPrint("remove"); + } + } + return removed; + } + + function insert(text, L) { + if (text) { + if (! inSplice) { + enterSplice(); + } + if (L) { + var newLines = Changeset.splitTextLines(text); + if (isCurLineInSplice()) { + //if (curCol == 0) { + //curSplice.length--; + //curSplice[1]--; + //Array.prototype.push.apply(curSplice, newLines); + //curLine += newLines.length; + //} + //else { + var sline = curSplice.length - 1; + var theLine = curSplice[sline]; + var lineCol = curCol; + curSplice[sline] = theLine.substring(0, lineCol) + newLines[0]; + curLine++; + newLines.splice(0, 1); + Array.prototype.push.apply(curSplice, newLines); + curLine += newLines.length; + curSplice.push(theLine.substring(lineCol)); + curCol = 0; + //} + } + else { + Array.prototype.push.apply(curSplice, newLines); + curLine += newLines.length; + } + } + else { + var sline = putCurLineInSplice(); + curSplice[sline] = curSplice[sline].substring(0, curCol) + + text + curSplice[sline].substring(curCol); + curCol += text.length; + } + //debugPrint("insert"); + } + } + + function hasMore() { + //print(lines.length+" / "+inSplice+" / "+(curSplice.length - 2)+" / "+curSplice[1]); + var docLines = lines_length(); + if (inSplice) { + docLines += curSplice.length - 2 - curSplice[1]; + } + return curLine < docLines; + } + + function close() { + if (inSplice) { + leaveSplice(); + } + //debugPrint("close"); + } + + var self = {skip:skip, remove:remove, insert:insert, close:close, hasMore:hasMore, + removeLines:removeLines, skipLines: skipLines}; + return self; +}; + +Changeset.applyZip = function(in1, idx1, in2, idx2, func) { + var iter1 = Changeset.opIterator(in1, idx1); + var iter2 = Changeset.opIterator(in2, idx2); + var assem = Changeset.smartOpAssembler(); + var op1 = Changeset.newOp(); + var op2 = Changeset.newOp(); + var opOut = Changeset.newOp(); + while (op1.opcode || iter1.hasNext() || op2.opcode || iter2.hasNext()) { + if ((! op1.opcode) && iter1.hasNext()) iter1.next(op1); + if ((! op2.opcode) && iter2.hasNext()) iter2.next(op2); + func(op1, op2, opOut); + if (opOut.opcode) { + //print(opOut.toSource()); + assem.append(opOut); + opOut.opcode = ''; + } + } + assem.endDocument(); + return assem.toString(); +}; + +Changeset.unpack = function(cs) { + var headerRegex = /Z:([0-9a-z]+)([><])([0-9a-z]+)|/; + var headerMatch = headerRegex.exec(cs); + if ((! headerMatch) || (! headerMatch[0])) { + Changeset.error("Not a changeset: "+cs); + } + var oldLen = Changeset.parseNum(headerMatch[1]); + var changeSign = (headerMatch[2] == '>') ? 1 : -1; + var changeMag = Changeset.parseNum(headerMatch[3]); + var newLen = oldLen + changeSign*changeMag; + var opsStart = headerMatch[0].length; + var opsEnd = cs.indexOf("$"); + if (opsEnd < 0) opsEnd = cs.length; + return {oldLen: oldLen, newLen: newLen, ops: cs.substring(opsStart, opsEnd), + charBank: cs.substring(opsEnd+1)}; +}; + +Changeset.pack = function(oldLen, newLen, opsStr, bank) { + var lenDiff = newLen - oldLen; + var lenDiffStr = (lenDiff >= 0 ? + '>'+Changeset.numToString(lenDiff) : + '<'+Changeset.numToString(-lenDiff)); + var a = []; + a.push('Z:', Changeset.numToString(oldLen), lenDiffStr, opsStr, '$', bank); + return a.join(''); +}; + +Changeset.applyToText = function(cs, str) { + var unpacked = Changeset.unpack(cs); + Changeset.assert(str.length == unpacked.oldLen, + "mismatched apply: ",str.length," / ",unpacked.oldLen); + var csIter = Changeset.opIterator(unpacked.ops); + var bankIter = Changeset.stringIterator(unpacked.charBank); + var strIter = Changeset.stringIterator(str); + var assem = Changeset.stringAssembler(); + while (csIter.hasNext()) { + var op = csIter.next(); + switch(op.opcode) { + case '+': assem.append(bankIter.take(op.chars)); break; + case '-': strIter.skip(op.chars); break; + case '=': assem.append(strIter.take(op.chars)); break; + } + } + assem.append(strIter.take(strIter.remaining())); + return assem.toString(); +}; + +Changeset.mutateTextLines = function(cs, lines) { + var unpacked = Changeset.unpack(cs); + var csIter = Changeset.opIterator(unpacked.ops); + var bankIter = Changeset.stringIterator(unpacked.charBank); + var mut = Changeset.textLinesMutator(lines); + while (csIter.hasNext()) { + var op = csIter.next(); + switch(op.opcode) { + case '+': mut.insert(bankIter.take(op.chars), op.lines); break; + case '-': mut.remove(op.chars, op.lines); break; + case '=': mut.skip(op.chars, op.lines, (!! op.attribs)); break; + } + } + mut.close(); +}; + +Changeset.composeAttributes = function(att1, att2, resultIsMutation, pool) { + // att1 and att2 are strings like "*3*f*1c", asMutation is a boolean. + + // Sometimes attribute (key,value) pairs are treated as attribute presence + // information, while other times they are treated as operations that + // mutate a set of attributes, and this affects whether an empty value + // is a deletion or a change. + // Examples, of the form (att1Items, att2Items, resultIsMutation) -> result + // ([], [(bold, )], true) -> [(bold, )] + // ([], [(bold, )], false) -> [] + // ([], [(bold, true)], true) -> [(bold, true)] + // ([], [(bold, true)], false) -> [(bold, true)] + // ([(bold, true)], [(bold, )], true) -> [(bold, )] + // ([(bold, true)], [(bold, )], false) -> [] + + // pool can be null if att2 has no attributes. + + if ((! att1) && resultIsMutation) { + // In the case of a mutation (i.e. composing two changesets), + // an att2 composed with an empy att1 is just att2. If att1 + // is part of an attribution string, then att2 may remove + // attributes that are already gone, so don't do this optimization. + return att2; + } + if (! att2) return att1; + var atts = []; + att1.replace(/\*([0-9a-z]+)/g, function(_, a) { + atts.push(pool.getAttrib(Changeset.parseNum(a))); + return ''; + }); + att2.replace(/\*([0-9a-z]+)/g, function(_, a) { + var pair = pool.getAttrib(Changeset.parseNum(a)); + var found = false; + for(var i=0;i"); + + var unpacked = Changeset.unpack(cs); + var csIter = Changeset.opIterator(unpacked.ops); + var csBank = unpacked.charBank; + var csBankIndex = 0; + // treat the attribution lines as text lines, mutating a line at a time + var mut = Changeset.textLinesMutator(lines); + + var lineIter = null; + function isNextMutOp() { + return (lineIter && lineIter.hasNext()) || mut.hasMore(); + } + function nextMutOp(destOp) { + if ((!(lineIter && lineIter.hasNext())) && mut.hasMore()) { + var line = mut.removeLines(1); + lineIter = Changeset.opIterator(line); + } + if (lineIter && lineIter.hasNext()) { + lineIter.next(destOp); + } + else { + destOp.opcode = ''; + } + } + var lineAssem = null; + function outputMutOp(op) { + //print("outputMutOp: "+op.toSource()); + if (! lineAssem) { + lineAssem = Changeset.mergingOpAssembler(); + } + lineAssem.append(op); + if (op.lines > 0) { + Changeset.assert(op.lines == 1, "Can't have op.lines of ",op.lines," in attribution lines"); + // ship it to the mut + mut.insert(lineAssem.toString(), 1); + lineAssem = null; + } + } + + var csOp = Changeset.newOp(); + var attOp = Changeset.newOp(); + var opOut = Changeset.newOp(); + while (csOp.opcode || csIter.hasNext() || attOp.opcode || isNextMutOp()) { + if ((! csOp.opcode) && csIter.hasNext()) { + csIter.next(csOp); + } + //print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource()); + //print(csOp.opcode+"/"+csOp.lines+"/"+csOp.attribs+"/"+lineAssem+"/"+lineIter+"/"+(lineIter?lineIter.hasNext():null)); + //print("csOp: "+csOp.toSource()); + if ((! csOp.opcode) && (! attOp.opcode) && + (! lineAssem) && (! (lineIter && lineIter.hasNext()))) { + break; // done + } + else if (csOp.opcode == '=' && csOp.lines > 0 && (! csOp.attribs) && (! attOp.opcode) && + (! lineAssem) && (! (lineIter && lineIter.hasNext()))) { + // skip multiple lines; this is what makes small changes not order of the document size + mut.skipLines(csOp.lines); + //print("skipped: "+csOp.lines); + csOp.opcode = ''; + } + else if (csOp.opcode == '+') { + if (csOp.lines > 1) { + var firstLineLen = csBank.indexOf('\n', csBankIndex) + 1 - csBankIndex; + Changeset.copyOp(csOp, opOut); + csOp.chars -= firstLineLen; + csOp.lines--; + opOut.lines = 1; + opOut.chars = firstLineLen; + } + else { + Changeset.copyOp(csOp, opOut); + csOp.opcode = ''; + } + outputMutOp(opOut); + csBankIndex += opOut.chars; + opOut.opcode = ''; + } + else { + if ((! attOp.opcode) && isNextMutOp()) { + nextMutOp(attOp); + } + //print("attOp: "+attOp.toSource()); + Changeset._slicerZipperFunc(attOp, csOp, opOut, pool); + if (opOut.opcode) { + outputMutOp(opOut); + opOut.opcode = ''; + } + } + } + + Changeset.assert(! lineAssem, "line assembler not finished"); + mut.close(); + + //dmesg("-> "+lines.toSource()); +}; + +Changeset.joinAttributionLines = function(theAlines) { + var assem = Changeset.mergingOpAssembler(); + for(var i=0;i 0) { + lines.push(assem.toString()); + assem.clear(); + } + pos += op.chars; + } + + while (iter.hasNext()) { + var op = iter.next(); + var numChars = op.chars; + var numLines = op.lines; + while (numLines > 1) { + var newlineEnd = text.indexOf('\n', pos)+1; + Changeset.assert(newlineEnd > 0, "newlineEnd <= 0 in splitAttributionLines"); + op.chars = newlineEnd - pos; + op.lines = 1; + appendOp(op); + numChars -= op.chars; + numLines -= op.lines; + } + if (numLines == 1) { + op.chars = numChars; + op.lines = 1; + } + appendOp(op); + } + + return lines; +}; + +Changeset.splitTextLines = function(text) { + return text.match(/[^\n]*(?:\n|[^\n]$)/g); +}; + +Changeset.compose = function(cs1, cs2, pool) { + var unpacked1 = Changeset.unpack(cs1); + var unpacked2 = Changeset.unpack(cs2); + var len1 = unpacked1.oldLen; + var len2 = unpacked1.newLen; + Changeset.assert(len2 == unpacked2.oldLen, "mismatched composition"); + var len3 = unpacked2.newLen; + var bankIter1 = Changeset.stringIterator(unpacked1.charBank); + var bankIter2 = Changeset.stringIterator(unpacked2.charBank); + var bankAssem = Changeset.stringAssembler(); + + var newOps = Changeset.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, function(op1, op2, opOut) { + //var debugBuilder = Changeset.stringAssembler(); + //debugBuilder.append(Changeset.opString(op1)); + //debugBuilder.append(','); + //debugBuilder.append(Changeset.opString(op2)); + //debugBuilder.append(' / '); + + var op1code = op1.opcode; + var op2code = op2.opcode; + if (op1code == '+' && op2code == '-') { + bankIter1.skip(Math.min(op1.chars, op2.chars)); + } + Changeset._slicerZipperFunc(op1, op2, opOut, pool); + if (opOut.opcode == '+') { + if (op2code == '+') { + bankAssem.append(bankIter2.take(opOut.chars)); + } + else { + bankAssem.append(bankIter1.take(opOut.chars)); + } + } + + //debugBuilder.append(Changeset.opString(op1)); + //debugBuilder.append(','); + //debugBuilder.append(Changeset.opString(op2)); + //debugBuilder.append(' -> '); + //debugBuilder.append(Changeset.opString(opOut)); + //print(debugBuilder.toString()); + }); + + return Changeset.pack(len1, len3, newOps, bankAssem.toString()); +}; + +Changeset.attributeTester = function(attribPair, pool) { + // returns a function that tests if a string of attributes + // (e.g. *3*4) contains a given attribute key,value that + // is already present in the pool. + if (! pool) { + return never; + } + var attribNum = pool.putAttrib(attribPair, true); + if (attribNum < 0) { + return never; + } + else { + var re = new RegExp('\\*'+Changeset.numToString(attribNum)+ + '(?!\\w)'); + return function(attribs) { + return re.test(attribs); + }; + } + function never(attribs) { return false; } +}; + +Changeset.identity = function(N) { + return Changeset.pack(N, N, "", ""); +}; + +Changeset.makeSplice = function(oldFullText, spliceStart, numRemoved, newText, optNewTextAPairs, pool) { + var oldLen = oldFullText.length; + + if (spliceStart >= oldLen) { + spliceStart = oldLen - 1; + } + if (numRemoved > oldFullText.length - spliceStart - 1) { + numRemoved = oldFullText.length - spliceStart - 1; + } + var oldText = oldFullText.substring(spliceStart, spliceStart+numRemoved); + var newLen = oldLen + newText.length - oldText.length; + + var assem = Changeset.smartOpAssembler(); + assem.appendOpWithText('=', oldFullText.substring(0, spliceStart)); + assem.appendOpWithText('-', oldText); + assem.appendOpWithText('+', newText, optNewTextAPairs, pool); + assem.endDocument(); + return Changeset.pack(oldLen, newLen, assem.toString(), newText); +}; + +Changeset.toSplices = function(cs) { + // get a list of splices, [startChar, endChar, newText] + + var unpacked = Changeset.unpack(cs); + var splices = []; + + var oldPos = 0; + var iter = Changeset.opIterator(unpacked.ops); + var charIter = Changeset.stringIterator(unpacked.charBank); + var inSplice = false; + while (iter.hasNext()) { + var op = iter.next(); + if (op.opcode == '=') { + oldPos += op.chars; + inSplice = false; + } + else { + if (! inSplice) { + splices.push([oldPos, oldPos, ""]); + inSplice = true; + } + if (op.opcode == '-') { + oldPos += op.chars; + splices[splices.length-1][1] += op.chars; + } + else if (op.opcode == '+') { + splices[splices.length-1][2] += charIter.take(op.chars); + } + } + } + + return splices; +}; + +Changeset.characterRangeFollow = function(cs, startChar, endChar, insertionsAfter) { + var newStartChar = startChar; + var newEndChar = endChar; + var splices = Changeset.toSplices(cs); + var lengthChangeSoFar = 0; + for(var i=0;i= newEndChar) { + // splice fully replaces/deletes range + // (also case that handles insertion at a collapsed selection) + if (insertionsAfter) { + newStartChar = newEndChar = spliceStart; + } + else { + newStartChar = newEndChar = spliceStart + newTextLength; + } + } + else if (spliceEnd <= newStartChar) { + // splice is before range + newStartChar += thisLengthChange; + newEndChar += thisLengthChange; + } + else if (spliceStart >= newEndChar) { + // splice is after range + } + else if (spliceStart >= newStartChar && spliceEnd <= newEndChar) { + // splice is inside range + newEndChar += thisLengthChange; + } + else if (spliceEnd < newEndChar) { + // splice overlaps beginning of range + newStartChar = spliceStart + newTextLength; + newEndChar += thisLengthChange; + } + else { + // splice overlaps end of range + newEndChar = spliceStart; + } + + lengthChangeSoFar += thisLengthChange; + } + + return [newStartChar, newEndChar]; +}; + +Changeset.moveOpsToNewPool = function(cs, oldPool, newPool) { + // works on changeset or attribution string + var dollarPos = cs.indexOf('$'); + if (dollarPos < 0) { + dollarPos = cs.length; + } + var upToDollar = cs.substring(0, dollarPos); + var fromDollar = cs.substring(dollarPos); + // order of attribs stays the same + return upToDollar.replace(/\*([0-9a-z]+)/g, function(_, a) { + var oldNum = Changeset.parseNum(a); + var pair = oldPool.getAttrib(oldNum); + var newNum = newPool.putAttrib(pair); + return '*'+Changeset.numToString(newNum); + }) + fromDollar; +}; + +Changeset.makeAttribution = function(text) { + var assem = Changeset.smartOpAssembler(); + assem.appendOpWithText('+', text); + return assem.toString(); +}; + +// callable on a changeset, attribution string, or attribs property of an op +Changeset.eachAttribNumber = function(cs, func) { + var dollarPos = cs.indexOf('$'); + if (dollarPos < 0) { + dollarPos = cs.length; + } + var upToDollar = cs.substring(0, dollarPos); + + upToDollar.replace(/\*([0-9a-z]+)/g, function(_, a) { + func(Changeset.parseNum(a)); + return ''; + }); +}; + +// callable on a changeset, attribution string, or attribs property of an op, +// though it may easily create adjacent ops that can be merged. +Changeset.filterAttribNumbers = function(cs, filter) { + return Changeset.mapAttribNumbers(cs, filter); +}; + +Changeset.mapAttribNumbers = function(cs, func) { + var dollarPos = cs.indexOf('$'); + if (dollarPos < 0) { + dollarPos = cs.length; + } + var upToDollar = cs.substring(0, dollarPos); + + var newUpToDollar = upToDollar.replace(/\*([0-9a-z]+)/g, function(s, a) { + var n = func(Changeset.parseNum(a)); + if (n === true) { + return s; + } + else if ((typeof n) === "number") { + return '*'+Changeset.numToString(n); + } + else { + return ''; + } + }); + + return newUpToDollar + cs.substring(dollarPos); +}; + +Changeset.makeAText = function(text, attribs) { + return { text: text, attribs: (attribs || Changeset.makeAttribution(text)) }; +}; + +Changeset.applyToAText = function(cs, atext, pool) { + return { text: Changeset.applyToText(cs, atext.text), + attribs: Changeset.applyToAttribution(cs, atext.attribs, pool) }; +}; + +Changeset.cloneAText = function(atext) { + return { text: atext.text, attribs: atext.attribs }; +}; + +Changeset.copyAText = function(atext1, atext2) { + atext2.text = atext1.text; + atext2.attribs = atext1.attribs; +}; + +Changeset.appendATextToAssembler = function(atext, assem) { + // intentionally skips last newline char of atext + var iter = Changeset.opIterator(atext.attribs); + var op = Changeset.newOp(); + while (iter.hasNext()) { + iter.next(op); + if (! iter.hasNext()) { + // last op, exclude final newline + if (op.lines <= 1) { + op.lines = 0; + op.chars--; + if (op.chars) { + assem.append(op); + } + } + else { + var nextToLastNewlineEnd = + atext.text.lastIndexOf('\n', atext.text.length-2) + 1; + var lastLineLength = atext.text.length - nextToLastNewlineEnd - 1; + op.lines--; + op.chars -= (lastLineLength + 1); + assem.append(op); + op.lines = 0; + op.chars = lastLineLength; + if (op.chars) { + assem.append(op); + } + } + } + else { + assem.append(op); + } + } +}; + +Changeset.prepareForWire = function(cs, pool) { + var newPool = new AttribPool(); + var newCs = Changeset.moveOpsToNewPool(cs, pool, newPool); + return {translated: newCs, pool: newPool}; +}; + +Changeset.isIdentity = function(cs) { + var unpacked = Changeset.unpack(cs); + return unpacked.ops == "" && unpacked.oldLen == unpacked.newLen; +}; + +Changeset.opAttributeValue = function(op, key, pool) { + return Changeset.attribsAttributeValue(op.attribs, key, pool); +}; + +Changeset.attribsAttributeValue = function(attribs, key, pool) { + var value = ''; + if (attribs) { + Changeset.eachAttribNumber(attribs, function(n) { + if (pool.getAttribKey(n) == key) { + value = pool.getAttribValue(n); + } + }); + } + return value; +}; + +Changeset.builder = function(oldLen) { + var assem = Changeset.smartOpAssembler(); + var o = Changeset.newOp(); + var charBank = Changeset.stringAssembler(); + + var self = { + // attribs are [[key1,value1],[key2,value2],...] or '*0*1...' (no pool needed in latter case) + keep: function(N, L, attribs, pool) { + o.opcode = '='; + o.attribs = (attribs && + Changeset.makeAttribsString('=', attribs, pool)) || ''; + o.chars = N; + o.lines = (L || 0); + assem.append(o); + return self; + }, + keepText: function(text, attribs, pool) { + assem.appendOpWithText('=', text, attribs, pool); + return self; + }, + insert: function(text, attribs, pool) { + assem.appendOpWithText('+', text, attribs, pool); + charBank.append(text); + return self; + }, + remove: function(N, L) { + o.opcode = '-'; + o.attribs = ''; + o.chars = N; + o.lines = (L || 0); + assem.append(o); + return self; + }, + toString: function() { + assem.endDocument(); + var newLen = oldLen + assem.getLengthChange(); + return Changeset.pack(oldLen, newLen, assem.toString(), + charBank.toString()); + } + }; + + return self; +}; + +Changeset.makeAttribsString = function(opcode, attribs, pool) { + // makeAttribsString(opcode, '*3') or makeAttribsString(opcode, [['foo','bar']], myPool) work + if (! attribs) { + return ''; + } + else if ((typeof attribs) == "string") { + return attribs; + } + else if (pool && attribs && attribs.length) { + if (attribs.length > 1) { + attribs = attribs.slice(); + attribs.sort(); + } + var result = []; + for(var i=0;i= attOp.chars && + attOp.lines > 0 && csOp.lines <= 0) { + csOp.lines++; + } + + Changeset._slicerZipperFunc(attOp, csOp, opOut, null); + if (opOut.opcode) { + assem.append(opOut); + opOut.opcode = ''; + } + } + } + } + + csOp.opcode = '-'; + csOp.chars = start; + + doCsOp(); + + if (optEnd === undefined) { + if (attOp.opcode) { + assem.append(attOp); + } + while (iter.hasNext()) { + iter.next(attOp); + assem.append(attOp); + } + } + else { + csOp.opcode = '='; + csOp.chars = optEnd - start; + doCsOp(); + } + + return assem.toString(); +}; + +Changeset.inverse = function(cs, lines, alines, pool) { + // lines and alines are what the changeset is meant to apply to. + // They may be arrays or objects with .get(i) and .length methods. + // They include final newlines on lines. + function lines_get(idx) { + if (lines.get) { + return lines.get(idx); + } + else { + return lines[idx]; + } + } + function lines_length() { + if ((typeof lines.length) == "number") { + return lines.length; + } + else { + return lines.length(); + } + } + function alines_get(idx) { + if (alines.get) { + return alines.get(idx); + } + else { + return alines[idx]; + } + } + function alines_length() { + if ((typeof alines.length) == "number") { + return alines.length; + } + else { + return alines.length(); + } + } + + var curLine = 0; + var curChar = 0; + var curLineOpIter = null; + var curLineOpIterLine; + var curLineNextOp = Changeset.newOp('+'); + + var unpacked = Changeset.unpack(cs); + var csIter = Changeset.opIterator(unpacked.ops); + var builder = Changeset.builder(unpacked.newLen); + + function consumeAttribRuns(numChars, func/*(len, attribs, endsLine)*/) { + + if ((! curLineOpIter) || (curLineOpIterLine != curLine)) { + // create curLineOpIter and advance it to curChar + curLineOpIter = Changeset.opIterator(alines_get(curLine)); + curLineOpIterLine = curLine; + var indexIntoLine = 0; + var done = false; + while (! done) { + curLineOpIter.next(curLineNextOp); + if (indexIntoLine + curLineNextOp.chars >= curChar) { + curLineNextOp.chars -= (curChar - indexIntoLine); + done = true; + } + else { + indexIntoLine += curLineNextOp.chars; + } + } + } + + while (numChars > 0) { + if ((! curLineNextOp.chars) && (! curLineOpIter.hasNext())) { + curLine++; + curChar = 0; + curLineOpIterLine = curLine; + curLineNextOp.chars = 0; + curLineOpIter = Changeset.opIterator(alines_get(curLine)); + } + if (! curLineNextOp.chars) { + curLineOpIter.next(curLineNextOp); + } + var charsToUse = Math.min(numChars, curLineNextOp.chars); + func(charsToUse, curLineNextOp.attribs, + charsToUse == curLineNextOp.chars && curLineNextOp.lines > 0); + numChars -= charsToUse; + curLineNextOp.chars -= charsToUse; + curChar += charsToUse; + } + + if ((! curLineNextOp.chars) && (! curLineOpIter.hasNext())) { + curLine++; + curChar = 0; + } + } + + function skip(N, L) { + if (L) { + curLine += L; + curChar = 0; + } + else { + if (curLineOpIter && curLineOpIterLine == curLine) { + consumeAttribRuns(N, function() {}); + } + else { + curChar += N; + } + } + } + + function nextText(numChars) { + var len = 0; + var assem = Changeset.stringAssembler(); + var firstString = lines_get(curLine).substring(curChar); + len += firstString.length; + assem.append(firstString); + + var lineNum = curLine+1; + while (len < numChars) { + var nextString = lines_get(lineNum); + len += nextString.length; + assem.append(nextString); + lineNum++; + } + + return assem.toString().substring(0, numChars); + } + + function cachedStrFunc(func) { + var cache = {}; + return function(s) { + if (! cache[s]) { + cache[s] = func(s); + } + return cache[s]; + }; + } + + var attribKeys = []; + var attribValues = []; + while (csIter.hasNext()) { + var csOp = csIter.next(); + if (csOp.opcode == '=') { + if (csOp.attribs) { + attribKeys.length = 0; + attribValues.length = 0; + Changeset.eachAttribNumber(csOp.attribs, function(n) { + attribKeys.push(pool.getAttribKey(n)); + attribValues.push(pool.getAttribValue(n)); + }); + var undoBackToAttribs = cachedStrFunc(function(attribs) { + var backAttribs = []; + for(var i=0;i 0) { + etherpad.betaSignupPageInit(); + } + + if ($('#productpage').size() > 0) { + etherpad.productPageInit(); + } + + if ($('.pricingpage').size() > 0) { + etherpad.pricingPageInit(); + } +}); + +etherpad = {}; + +//---------------------------------------------------------------- +// general utils +//---------------------------------------------------------------- + +etherpad.validEmail = function(x) { + return (x.length > 0 && + x.match(/^[\w\.\_\+\-]+\@[\w\_\-]+\.[\w\_\-\.]+$/)); +}; + +//---------------------------------------------------------------- +// obfuscating emails +//---------------------------------------------------------------- + +etherpad.deobfuscateEmails = function() { + $("a.obfuscemail").each(function() { + $(this).html($(this).html().replace('p*d.sp***e','pad.spline')); + this.href = this.href.replace('p*d.sp***e','pad.spline'); + }); +}; + +//---------------------------------------------------------------- +// Signing up for pricing info +//---------------------------------------------------------------- + +etherpad.pricingPageInit = function() { + $('#submitbutton').click(etherpad.pricingSubmit); +}; + +etherpad.pricingSubmit = function(edition) { + var allData = {}; + $('#pricingcontact input.ti').each(function() { + allData[$(this).attr('id')] = $(this).val(); + }); + allData.industry = $('#industry').val(); + + $('form button').hide(); + $('#spinner').show(); + $('form input').attr('disabled', true); + + $.ajax({ + type: 'post', + url: $('#pricingcontact').attr('action'), + data: allData, + success: success, + error: error + }); + + function success(responseText) { + $('#spinner').hide(); + if (responseText == "OK") { + $('#errorbox').hide(); + $('#confirmbox').fadeIn('fast'); + } else { + $('#confirmbox').hide(); + $('#errorbox').hide().html(responseText).fadeIn('fast'); + $('form button').show(); + $('form input').removeAttr('disabled'); + } + } + function error() { + $('#spinner').hide(); + $('#errorbox').hide().html("Server error.").fadeIn('fast'); + $('form button').show(); + $('form input').removeAttr('disabled'); + } + + return false; +} + + +//---------------------------------------------------------------- +// Product page (client-side nagivation with JS) +//---------------------------------------------------------------- + +etherpad.productPageInit = function() { + $("#productpage #tour").addClass("javascripton"); + etherpad.productPageNavigateTo(window.location.hash.substring(1)); + + $("#productpage a.tournav").click(etherpad.tourNavClick); +} + +etherpad.tourNavClick = function() { // to be called as a click event handler + var href = $(this).attr('href'); + var thorpLoc = href.indexOf('#'); + if (thorpLoc >= 0) { + etherpad.productPageNavigateTo(href.substring(thorpLoc+1), true); + } +} + +etherpad.productPageNavigateTo = function(hash, shouldAnimate) { + function setNavLink(rightOrLeft, text, linkhash) { + var navcells = $('#productpage .tourbar .'+rightOrLeft); + if (! text) { + navcells.html(' '); + } + else { + navcells. + html(''+text+''). + find('a.tournav').click(etherpad.tourNavClick); + } + } + function switchCardsIfNecessary(fromCard, toCard, andThen/*(didAnimate)*/) { + if (! $('#productpage #tour').hasClass("show"+toCard)) { + var afterAnimate = function() { + $("#productpage #"+fromCard).get(0).style.display = ""; + $('#productpage #tour').removeClass("show"+fromCard).addClass("show"+toCard); + if (andThen) andThen(shouldAnimate); + } + if (shouldAnimate) { + $("#productpage #"+fromCard).fadeOut("fast", afterAnimate); + } + else { + afterAnimate(); + } + } + else { + andThen(false); + } + } + function switchProseIfNecessary(toNum, useAnimation, andThen) { + var visibleProse = $("#productpage .tourprose:visible"); + var alreadyVisible = ($("#productpage #tour"+toNum+"prose:visible").size() > 0); + function assignVisibilities() { + $("#productpage .tourprose").each(function() { + if (this.id == "tour"+toNum+"prose") { + this.style.display = 'block'; + } + else { + this.style.display = 'none'; + } + }); + } + + if ((! useAnimation) || visibleProse.size() == 0 || alreadyVisible) { + assignVisibilities(); + andThen(); + } + else { + function afterAnimate() { + assignVisibilities(); + andThen(); + } + if (visibleProse.size() > 0 && visibleProse.get(0).id != "tour"+toNum+"prose") { + visibleProse.fadeOut("fast", afterAnimate); + } + else { + afterAnimate(); + } + } + } + function getProseTitle(n) { + if (n == 0) return clientVars.screenshotTitle; + var atag = $("#productpage #tourleftnav .tour"+n+" a"); + if (atag.size() > 0) return atag.text(); + return ''; + } + + var regexResult; + if ((regexResult = /^uses([1-9][0-9]*)$/.exec(hash))) { + var tourNum = +regexResult[1]; + switchCardsIfNecessary("pageshot", "usecases", function(didAnimate) { + switchProseIfNecessary(tourNum, shouldAnimate && !didAnimate, function() { + /*var n = tourNum; + setNavLink("left", "« "+getProseTitle(n-1), (n == 1 ? "" : "uses"+(n-1))); + var nextTitle = getProseTitle(n+1); + if (! nextTitle) setNavLink("right", ""); + else setNavLink("right", nextTitle+" »", "uses"+(n+1));*/ + /*setNavLink("left", "« "+getProseTitle(0), ""); + setNavLink("right", "");*/ + setNavLink("right", "« "+getProseTitle(0), ""); + $('#tourtop td.left').html("Use Cases"); + $("#productpage #tourleftnav li").removeClass("selected"); + $("#productpage #tourleftnav li.tour"+tourNum).addClass("selected"); + }); + }); + } + else { + switchCardsIfNecessary("usecases", "pageshot", function() { + $('#tourtop td.left').html(getProseTitle(0)); + setNavLink("right", clientVars.screenshotNextLink, "uses1"); + }); + } +} diff --git a/trunk/etherpad/src/static/js/jquery-1.2.6.js b/trunk/etherpad/src/static/js/jquery-1.2.6.js new file mode 100755 index 0000000..88e661e --- /dev/null +++ b/trunk/etherpad/src/static/js/jquery-1.2.6.js @@ -0,0 +1,3549 @@ +(function(){ +/* + * jQuery 1.2.6 - New Wave Javascript + * + * Copyright (c) 2008 John Resig (jquery.com) + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + * + * $Date: 2008-05-24 14:22:17 -0400 (Sat, 24 May 2008) $ + * $Rev: 5685 $ + */ + +// Map over jQuery in case of overwrite +var _jQuery = window.jQuery, +// Map over the $ in case of overwrite + _$ = window.$; + +var jQuery = window.jQuery = window.$ = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + return new jQuery.fn.init( selector, context ); +}; + +// A simple way to check for HTML strings or ID strings +// (both of which we optimize for) +var quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^#(\w+)$/, + +// Is it a simple selector + isSimple = /^.[^:#\[\.]*$/, + +// Will speed up references to undefined, and allows munging its name. + undefined; + +jQuery.fn = jQuery.prototype = { + init: function( selector, context ) { + // Make sure that a selection was provided + selector = selector || document; + + // Handle $(DOMElement) + if ( selector.nodeType ) { + this[0] = selector; + this.length = 1; + return this; + } + // Handle HTML strings + if ( typeof selector == "string" ) { + // Are we dealing with HTML string or an ID? + var match = quickExpr.exec( selector ); + + // Verify a match, and that no context was specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) + selector = jQuery.clean( [ match[1] ], context ); + + // HANDLE: $("#id") + else { + var elem = document.getElementById( match[3] ); + + // Make sure an element was located + if ( elem ){ + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id != match[3] ) + return jQuery().find( selector ); + + // Otherwise, we inject the element directly into the jQuery object + return jQuery( elem ); + } + selector = []; + } + + // HANDLE: $(expr, [context]) + // (which is just equivalent to: $(content).find(expr) + } else + return jQuery( context ).find( selector ); + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) + return jQuery( document )[ jQuery.fn.ready ? "ready" : "load" ]( selector ); + + return this.setArray(jQuery.makeArray(selector)); + }, + + // The current version of jQuery being used + jquery: "1.2.6", + + // The number of elements contained in the matched element set + size: function() { + return this.length; + }, + + // The number of elements contained in the matched element set + length: 0, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num == undefined ? + + // Return a 'clean' array + jQuery.makeArray( this ) : + + // Return just the object + this[ num ]; + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems ) { + // Build a new jQuery matched element set + var ret = jQuery( elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + // Return the newly-formed element set + return ret; + }, + + // Force the current matched set of elements to become + // the specified array of elements (destroying the stack in the process) + // You should use pushStack() in order to do this, but maintain the stack + setArray: function( elems ) { + // Resetting the length to 0, then using the native Array push + // is a super-fast way to populate an object with array-like properties + this.length = 0; + Array.prototype.push.apply( this, elems ); + + return this; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function( callback, args ) { + return jQuery.each( this, callback, args ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + var ret = -1; + + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem && elem.jquery ? elem[0] : elem + , this ); + }, + + attr: function( name, value, type ) { + var options = name; + + // Look for the case where we're accessing a style value + if ( name.constructor == String ) + if ( value === undefined ) + return this[0] && jQuery[ type || "attr" ]( this[0], name ); + + else { + options = {}; + options[ name ] = value; + } + + // Check to see if we're setting style values + return this.each(function(i){ + // Set all the styles + for ( name in options ) + jQuery.attr( + type ? + this.style : + this, + name, jQuery.prop( this, options[ name ], type, i, name ) + ); + }); + }, + + css: function( key, value ) { + // ignore negative width and height values + if ( (key == 'width' || key == 'height') && parseFloat(value) < 0 ) + value = undefined; + return this.attr( key, value, "curCSS" ); + }, + + text: function( text ) { + if ( typeof text != "object" && text != null ) + return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) ); + + var ret = ""; + + jQuery.each( text || this, function(){ + jQuery.each( this.childNodes, function(){ + if ( this.nodeType != 8 ) + ret += this.nodeType != 1 ? + this.nodeValue : + jQuery.fn.text( [ this ] ); + }); + }); + + return ret; + }, + + wrapAll: function( html ) { + if ( this[0] ) + // The elements to wrap the target around + jQuery( html, this[0].ownerDocument ) + .clone() + .insertBefore( this[0] ) + .map(function(){ + var elem = this; + + while ( elem.firstChild ) + elem = elem.firstChild; + + return elem; + }) + .append(this); + + return this; + }, + + wrapInner: function( html ) { + return this.each(function(){ + jQuery( this ).contents().wrapAll( html ); + }); + }, + + wrap: function( html ) { + return this.each(function(){ + jQuery( this ).wrapAll( html ); + }); + }, + + append: function() { + return this.domManip(arguments, true, false, function(elem){ + if (this.nodeType == 1) + this.appendChild( elem ); + }); + }, + + prepend: function() { + return this.domManip(arguments, true, true, function(elem){ + if (this.nodeType == 1) + this.insertBefore( elem, this.firstChild ); + }); + }, + + before: function() { + return this.domManip(arguments, false, false, function(elem){ + this.parentNode.insertBefore( elem, this ); + }); + }, + + after: function() { + return this.domManip(arguments, false, true, function(elem){ + this.parentNode.insertBefore( elem, this.nextSibling ); + }); + }, + + end: function() { + return this.prevObject || jQuery( [] ); + }, + + find: function( selector ) { + var elems = jQuery.map(this, function(elem){ + return jQuery.find( selector, elem ); + }); + + return this.pushStack( /[^+>] [^+>]/.test( selector ) || selector.indexOf("..") > -1 ? + jQuery.unique( elems ) : + elems ); + }, + + clone: function( events ) { + // Do the clone + var ret = this.map(function(){ + if ( jQuery.browser.msie && !jQuery.isXMLDoc(this) ) { + // IE copies events bound via attachEvent when + // using cloneNode. Calling detachEvent on the + // clone will also remove the events from the orignal + // In order to get around this, we use innerHTML. + // Unfortunately, this means some modifications to + // attributes in IE that are actually only stored + // as properties will not be copied (such as the + // the name attribute on an input). + var clone = this.cloneNode(true), + container = document.createElement("div"); + container.appendChild(clone); + return jQuery.clean([container.innerHTML])[0]; + } else + return this.cloneNode(true); + }); + + // Need to set the expando to null on the cloned set if it exists + // removeData doesn't work here, IE removes it from the original as well + // this is primarily for IE but the data expando shouldn't be copied over in any browser + var clone = ret.find("*").andSelf().each(function(){ + if ( this[ expando ] != undefined ) + this[ expando ] = null; + }); + + // Copy the events from the original to the clone + if ( events === true ) + this.find("*").andSelf().each(function(i){ + if (this.nodeType == 3) + return; + var events = jQuery.data( this, "events" ); + + for ( var type in events ) + for ( var handler in events[ type ] ) + jQuery.event.add( clone[ i ], type, events[ type ][ handler ], events[ type ][ handler ].data ); + }); + + // Return the cloned set + return ret; + }, + + filter: function( selector ) { + return this.pushStack( + jQuery.isFunction( selector ) && + jQuery.grep(this, function(elem, i){ + return selector.call( elem, i ); + }) || + + jQuery.multiFilter( selector, this ) ); + }, + + not: function( selector ) { + if ( selector.constructor == String ) + // test special case where just one selector is passed in + if ( isSimple.test( selector ) ) + return this.pushStack( jQuery.multiFilter( selector, this, true ) ); + else + selector = jQuery.multiFilter( selector, this ); + + var isArrayLike = selector.length && selector[selector.length - 1] !== undefined && !selector.nodeType; + return this.filter(function() { + return isArrayLike ? jQuery.inArray( this, selector ) < 0 : this != selector; + }); + }, + + add: function( selector ) { + return this.pushStack( jQuery.unique( jQuery.merge( + this.get(), + typeof selector == 'string' ? + jQuery( selector ) : + jQuery.makeArray( selector ) + ))); + }, + + is: function( selector ) { + return !!selector && jQuery.multiFilter( selector, this ).length > 0; + }, + + hasClass: function( selector ) { + return this.is( "." + selector ); + }, + + val: function( value ) { + if ( value == undefined ) { + + if ( this.length ) { + var elem = this[0]; + + // We need to handle select boxes special + if ( jQuery.nodeName( elem, "select" ) ) { + var index = elem.selectedIndex, + values = [], + options = elem.options, + one = elem.type == "select-one"; + + // Nothing was selected + if ( index < 0 ) + return null; + + // Loop through all the selected options + for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) { + var option = options[ i ]; + + if ( option.selected ) { + // Get the specifc value for the option + value = jQuery.browser.msie && !option.attributes.value.specified ? option.text : option.value; + + // We don't need an array for one selects + if ( one ) + return value; + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + + // Everything else, we just grab the value + } else + return (this[0].value || "").replace(/\r/g, ""); + + } + + return undefined; + } + + if( value.constructor == Number ) + value += ''; + + return this.each(function(){ + if ( this.nodeType != 1 ) + return; + + if ( value.constructor == Array && /radio|checkbox/.test( this.type ) ) + this.checked = (jQuery.inArray(this.value, value) >= 0 || + jQuery.inArray(this.name, value) >= 0); + + else if ( jQuery.nodeName( this, "select" ) ) { + var values = jQuery.makeArray(value); + + jQuery( "option", this ).each(function(){ + this.selected = (jQuery.inArray( this.value, values ) >= 0 || + jQuery.inArray( this.text, values ) >= 0); + }); + + if ( !values.length ) + this.selectedIndex = -1; + + } else + this.value = value; + }); + }, + + html: function( value ) { + return value == undefined ? + (this[0] ? + this[0].innerHTML : + null) : + this.empty().append( value ); + }, + + replaceWith: function( value ) { + return this.after( value ).remove(); + }, + + eq: function( i ) { + return this.slice( i, i + 1 ); + }, + + slice: function() { + return this.pushStack( Array.prototype.slice.apply( this, arguments ) ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map(this, function(elem, i){ + return callback.call( elem, i, elem ); + })); + }, + + andSelf: function() { + return this.add( this.prevObject ); + }, + + data: function( key, value ){ + var parts = key.split("."); + parts[1] = parts[1] ? "." + parts[1] : ""; + + if ( value === undefined ) { + var data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]); + + if ( data === undefined && this.length ) + data = jQuery.data( this[0], key ); + + return data === undefined && parts[1] ? + this.data( parts[0] ) : + data; + } else + return this.trigger("setData" + parts[1] + "!", [parts[0], value]).each(function(){ + jQuery.data( this, key, value ); + }); + }, + + removeData: function( key ){ + return this.each(function(){ + jQuery.removeData( this, key ); + }); + }, + + domManip: function( args, table, reverse, callback ) { + var clone = this.length > 1, elems; + + return this.each(function(){ + if ( !elems ) { + elems = jQuery.clean( args, this.ownerDocument ); + + if ( reverse ) + elems.reverse(); + } + + var obj = this; + + if ( table && jQuery.nodeName( this, "table" ) && jQuery.nodeName( elems[0], "tr" ) ) + obj = this.getElementsByTagName("tbody")[0] || this.appendChild( this.ownerDocument.createElement("tbody") ); + + var scripts = jQuery( [] ); + + jQuery.each(elems, function(){ + var elem = clone ? + jQuery( this ).clone( true )[0] : + this; + + // execute all scripts after the elements have been injected + if ( jQuery.nodeName( elem, "script" ) ) + scripts = scripts.add( elem ); + else { + // Remove any inner scripts for later evaluation + if ( elem.nodeType == 1 ) + scripts = scripts.add( jQuery( "script", elem ).remove() ); + + // Inject the elements into the document + callback.call( obj, elem ); + } + }); + + scripts.each( evalScript ); + }); + } +}; + +// Give the init function the jQuery prototype for later instantiation +jQuery.fn.init.prototype = jQuery.fn; + +function evalScript( i, elem ) { + if ( elem.src ) + jQuery.ajax({ + url: elem.src, + async: false, + dataType: "script" + }); + + else + jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" ); + + if ( elem.parentNode ) + elem.parentNode.removeChild( elem ); +} + +function now(){ + return +new Date; +} + +jQuery.extend = jQuery.fn.extend = function() { + // copy reference to target object + var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options; + + // Handle a deep copy situation + if ( target.constructor == Boolean ) { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target != "object" && typeof target != "function" ) + target = {}; + + // extend jQuery itself if only one argument is passed + if ( length == i ) { + target = this; + --i; + } + + for ( ; i < length; i++ ) + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) + // Extend the base object + for ( var name in options ) { + var src = target[ name ], copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) + continue; + + // Recurse if we're merging object values + if ( deep && copy && typeof copy == "object" && !copy.nodeType ) + target[ name ] = jQuery.extend( deep, + // Never move original objects, clone them + src || ( copy.length != null ? [ ] : { } ) + , copy ); + + // Don't bring in undefined values + else if ( copy !== undefined ) + target[ name ] = copy; + + } + + // Return the modified object + return target; +}; + +var expando = "jQuery" + now(), uuid = 0, windowData = {}, + // exclude the following css properties to add px + exclude = /z-?index|font-?weight|opacity|zoom|line-?height/i, + // cache defaultView + defaultView = document.defaultView || {}; + +jQuery.extend({ + noConflict: function( deep ) { + window.$ = _$; + + if ( deep ) + window.jQuery = _jQuery; + + return jQuery; + }, + + // See test/unit/core.js for details concerning this function. + isFunction: function( fn ) { + return !!fn && typeof fn != "string" && !fn.nodeName && + fn.constructor != Array && /^[\s[]?function/.test( fn + "" ); + }, + + // check if an element is in a (or is an) XML document + isXMLDoc: function( elem ) { + return elem.documentElement && !elem.body || + elem.tagName && elem.ownerDocument && !elem.ownerDocument.body; + }, + + // Evalulates a script in a global context + globalEval: function( data ) { + data = jQuery.trim( data ); + + if ( data ) { + // Inspired by code by Andrea Giammarchi + // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html + var head = document.getElementsByTagName("head")[0] || document.documentElement, + script = document.createElement("script"); + + script.type = "text/javascript"; + if ( jQuery.browser.msie ) + script.text = data; + else + script.appendChild( document.createTextNode( data ) ); + + // Use insertBefore instead of appendChild to circumvent an IE6 bug. + // This arises when a base node is used (#2709). + head.insertBefore( script, head.firstChild ); + head.removeChild( script ); + } + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toUpperCase() == name.toUpperCase(); + }, + + cache: {}, + + data: function( elem, name, data ) { + elem = elem == window ? + windowData : + elem; + + var id = elem[ expando ]; + + // Compute a unique ID for the element + if ( !id ) + id = elem[ expando ] = ++uuid; + + // Only generate the data cache if we're + // trying to access or manipulate it + if ( name && !jQuery.cache[ id ] ) + jQuery.cache[ id ] = {}; + + // Prevent overriding the named cache with undefined values + if ( data !== undefined ) + jQuery.cache[ id ][ name ] = data; + + // Return the named cache data, or the ID for the element + return name ? + jQuery.cache[ id ][ name ] : + id; + }, + + removeData: function( elem, name ) { + elem = elem == window ? + windowData : + elem; + + var id = elem[ expando ]; + + // If we want to remove a specific section of the element's data + if ( name ) { + if ( jQuery.cache[ id ] ) { + // Remove the section of cache data + delete jQuery.cache[ id ][ name ]; + + // If we've removed all the data, remove the element's cache + name = ""; + + for ( name in jQuery.cache[ id ] ) + break; + + if ( !name ) + jQuery.removeData( elem ); + } + + // Otherwise, we want to remove all of the element's data + } else { + // Clean up the element expando + try { + delete elem[ expando ]; + } catch(e){ + // IE has trouble directly removing the expando + // but it's ok with using removeAttribute + if ( elem.removeAttribute ) + elem.removeAttribute( expando ); + } + + // Completely remove the data cache + delete jQuery.cache[ id ]; + } + }, + + // args is for internal usage only + each: function( object, callback, args ) { + var name, i = 0, length = object.length; + + if ( args ) { + if ( length == undefined ) { + for ( name in object ) + if ( callback.apply( object[ name ], args ) === false ) + break; + } else + for ( ; i < length; ) + if ( callback.apply( object[ i++ ], args ) === false ) + break; + + // A special, fast, case for the most common use of each + } else { + if ( length == undefined ) { + for ( name in object ) + if ( callback.call( object[ name ], name, object[ name ] ) === false ) + break; + } else + for ( var value = object[0]; + i < length && callback.call( value, i, value ) !== false; value = object[++i] ){} + } + + return object; + }, + + prop: function( elem, value, type, i, name ) { + // Handle executable functions + if ( jQuery.isFunction( value ) ) + value = value.call( elem, i ); + + // Handle passing in a number to a CSS property + return value && value.constructor == Number && type == "curCSS" && !exclude.test( name ) ? + value + "px" : + value; + }, + + className: { + // internal only, use addClass("class") + add: function( elem, classNames ) { + jQuery.each((classNames || "").split(/\s+/), function(i, className){ + if ( elem.nodeType == 1 && !jQuery.className.has( elem.className, className ) ) + elem.className += (elem.className ? " " : "") + className; + }); + }, + + // internal only, use removeClass("class") + remove: function( elem, classNames ) { + if (elem.nodeType == 1) + elem.className = classNames != undefined ? + jQuery.grep(elem.className.split(/\s+/), function(className){ + return !jQuery.className.has( classNames, className ); + }).join(" ") : + ""; + }, + + // internal only, use hasClass("class") + has: function( elem, className ) { + return jQuery.inArray( className, (elem.className || elem).toString().split(/\s+/) ) > -1; + } + }, + + // A method for quickly swapping in/out CSS properties to get correct calculations + swap: function( elem, options, callback ) { + var old = {}; + // Remember the old values, and insert the new ones + for ( var name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + callback.call( elem ); + + // Revert the old values + for ( var name in options ) + elem.style[ name ] = old[ name ]; + }, + + css: function( elem, name, force ) { + if ( name == "width" || name == "height" ) { + var val, props = { position: "absolute", visibility: "hidden", display:"block" }, which = name == "width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ]; + + function getWH() { + val = name == "width" ? elem.offsetWidth : elem.offsetHeight; + var padding = 0, border = 0; + jQuery.each( which, function() { + padding += parseFloat(jQuery.curCSS( elem, "padding" + this, true)) || 0; + border += parseFloat(jQuery.curCSS( elem, "border" + this + "Width", true)) || 0; + }); + val -= Math.round(padding + border); + } + + if ( jQuery(elem).is(":visible") ) + getWH(); + else + jQuery.swap( elem, props, getWH ); + + return Math.max(0, val); + } + + return jQuery.curCSS( elem, name, force ); + }, + + curCSS: function( elem, name, force ) { + var ret, style = elem.style; + + // A helper method for determining if an element's values are broken + function color( elem ) { + if ( !jQuery.browser.safari ) + return false; + + // defaultView is cached + var ret = defaultView.getComputedStyle( elem, null ); + return !ret || ret.getPropertyValue("color") == ""; + } + + // We need to handle opacity special in IE + if ( name == "opacity" && jQuery.browser.msie ) { + ret = jQuery.attr( style, "opacity" ); + + return ret == "" ? + "1" : + ret; + } + // Opera sometimes will give the wrong display answer, this fixes it, see #2037 + if ( jQuery.browser.opera && name == "display" ) { + var save = style.outline; + style.outline = "0 solid black"; + style.outline = save; + } + + // Make sure we're using the right name for getting the float value + if ( name.match( /float/i ) ) + name = styleFloat; + + if ( !force && style && style[ name ] ) + ret = style[ name ]; + + else if ( defaultView.getComputedStyle ) { + + // Only "float" is needed here + if ( name.match( /float/i ) ) + name = "float"; + + name = name.replace( /([A-Z])/g, "-$1" ).toLowerCase(); + + var computedStyle = defaultView.getComputedStyle( elem, null ); + + if ( computedStyle && !color( elem ) ) + ret = computedStyle.getPropertyValue( name ); + + // If the element isn't reporting its values properly in Safari + // then some display: none elements are involved + else { + var swap = [], stack = [], a = elem, i = 0; + + // Locate all of the parent display: none elements + for ( ; a && color(a); a = a.parentNode ) + stack.unshift(a); + + // Go through and make them visible, but in reverse + // (It would be better if we knew the exact display type that they had) + for ( ; i < stack.length; i++ ) + if ( color( stack[ i ] ) ) { + swap[ i ] = stack[ i ].style.display; + stack[ i ].style.display = "block"; + } + + // Since we flip the display style, we have to handle that + // one special, otherwise get the value + ret = name == "display" && swap[ stack.length - 1 ] != null ? + "none" : + ( computedStyle && computedStyle.getPropertyValue( name ) ) || ""; + + // Finally, revert the display styles back + for ( i = 0; i < swap.length; i++ ) + if ( swap[ i ] != null ) + stack[ i ].style.display = swap[ i ]; + } + + // We should always get a number back from opacity + if ( name == "opacity" && ret == "" ) + ret = "1"; + + } else if ( elem.currentStyle ) { + var camelCase = name.replace(/\-(\w)/g, function(all, letter){ + return letter.toUpperCase(); + }); + + ret = elem.currentStyle[ name ] || elem.currentStyle[ camelCase ]; + + // From the awesome hack by Dean Edwards + // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 + + // If we're not dealing with a regular pixel number + // but a number that has a weird ending, we need to convert it to pixels + if ( !/^\d+(px)?$/i.test( ret ) && /^\d/.test( ret ) ) { + // Remember the original values + var left = style.left, rsLeft = elem.runtimeStyle.left; + + // Put in the new values to get a computed value out + elem.runtimeStyle.left = elem.currentStyle.left; + style.left = ret || 0; + ret = style.pixelLeft + "px"; + + // Revert the changed values + style.left = left; + elem.runtimeStyle.left = rsLeft; + } + } + + return ret; + }, + + clean: function( elems, context ) { + var ret = []; + context = context || document; + // !context.createElement fails in IE with an error but returns typeof 'object' + if (typeof context.createElement == 'undefined') + context = context.ownerDocument || context[0] && context[0].ownerDocument || document; + + jQuery.each(elems, function(i, elem){ + if ( !elem ) + return; + + if ( elem.constructor == Number ) + elem += ''; + + // Convert html string into DOM nodes + if ( typeof elem == "string" ) { + // Fix "XHTML"-style tags in all browsers + elem = elem.replace(/(<(\w+)[^>]*?)\/>/g, function(all, front, tag){ + return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i) ? + all : + front + ">"; + }); + + // Trim whitespace, otherwise indexOf won't work as expected + var tags = jQuery.trim( elem ).toLowerCase(), div = context.createElement("div"); + + var wrap = + // option or optgroup + !tags.indexOf("", "" ] || + + !tags.indexOf("", "" ] || + + tags.match(/^<(thead|tbody|tfoot|colg|cap)/) && + [ 1, "", "
" ] || + + !tags.indexOf("", "" ] || + + // matched above + (!tags.indexOf("", "" ] || + + !tags.indexOf("", "" ] || + + // IE can't serialize and