first-commit

This commit is contained in:
Peter 'Pita' Martischka 2011-03-26 13:10:41 +00:00
commit 325c322a27
207 changed files with 35989 additions and 0 deletions

253
static/js/ace.js Normal file
View file

@ -0,0 +1,253 @@
/**
* 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.
*/
// requires: top
// requires: plugins
// requires: undefined
Ace2Editor.registry = { nextId: 1 };
function Ace2Editor() {
var thisFunctionsName = "Ace2Editor";
var ace2 = Ace2Editor;
var editor = {};
var info = { editor: editor, id: (ace2.registry.nextId++) };
var loaded = false;
var actionsPendingInit = [];
function pendingInit(func, optDoNow) {
return function() {
var that = this;
var args = arguments;
function action() {
func.apply(that, args);
}
if (optDoNow) {
optDoNow.apply(that, args);
}
if (loaded) {
action();
}
else {
actionsPendingInit.push(action);
}
};
}
function doActionsPendingInit() {
for(var i=0;i<actionsPendingInit.length;i++) {
actionsPendingInit[i]();
}
actionsPendingInit = [];
}
ace2.registry[info.id] = info;
editor.importText = pendingInit(function(newCode, undoable) {
info.ace_importText(newCode, undoable); });
editor.importAText = pendingInit(function(newCode, apoolJsonObj, undoable) {
info.ace_importAText(newCode, apoolJsonObj, undoable); });
editor.exportText = function() {
if (! loaded) return "(awaiting init)\n";
return info.ace_exportText();
};
editor.getFrame = function() { return info.frame || null; };
editor.focus = pendingInit(function() { info.ace_focus(); });
editor.setEditable = pendingInit(function(newVal) { info.ace_setEditable(newVal); });
editor.getFormattedCode = function() { return info.ace_getFormattedCode(); };
editor.setOnKeyPress = pendingInit(function (handler) { info.ace_setOnKeyPress(handler); });
editor.setOnKeyDown = pendingInit(function (handler) { info.ace_setOnKeyDown(handler); });
editor.setNotifyDirty = pendingInit(function (handler) { info.ace_setNotifyDirty(handler); });
editor.setProperty = pendingInit(function(key, value) { info.ace_setProperty(key, value); });
editor.getDebugProperty = function(prop) { return info.ace_getDebugProperty(prop); };
editor.setBaseText = pendingInit(function(txt) { info.ace_setBaseText(txt); });
editor.setBaseAttributedText = pendingInit(function(atxt, apoolJsonObj) {
info.ace_setBaseAttributedText(atxt, apoolJsonObj); });
editor.applyChangesToBase = pendingInit(function (changes, optAuthor,apoolJsonObj) {
info.ace_applyChangesToBase(changes, optAuthor, apoolJsonObj); });
// prepareUserChangeset:
// Returns null if no new changes or ACE not ready. Otherwise, bundles up all user changes
// to the latest base text into a Changeset, which is returned (as a string if encodeAsString).
// If this method returns a truthy value, then applyPreparedChangesetToBase can be called
// at some later point to consider these changes part of the base, after which prepareUserChangeset
// must be called again before applyPreparedChangesetToBase. Multiple consecutive calls
// to prepareUserChangeset will return an updated changeset that takes into account the
// latest user changes, and modify the changeset to be applied by applyPreparedChangesetToBase
// accordingly.
editor.prepareUserChangeset = function() {
if (! loaded) return null;
return info.ace_prepareUserChangeset();
};
editor.applyPreparedChangesetToBase = pendingInit(
function() { info.ace_applyPreparedChangesetToBase(); });
editor.setUserChangeNotificationCallback = pendingInit(function(callback) {
info.ace_setUserChangeNotificationCallback(callback);
});
editor.setAuthorInfo = pendingInit(function(author, authorInfo) {
info.ace_setAuthorInfo(author, authorInfo);
});
editor.setAuthorSelectionRange = pendingInit(function(author, start, end) {
info.ace_setAuthorSelectionRange(author, start, end);
});
editor.getUnhandledErrors = function() {
if (! loaded) return [];
// returns array of {error: <browser Error object>, time: +new Date()}
return info.ace_getUnhandledErrors();
};
editor.callWithAce = pendingInit(function(fn, callStack, normalize) {
return info.ace_callWithAce(fn, callStack, normalize);
});
editor.execCommand = pendingInit(function(cmd, arg1) {
info.ace_execCommand(cmd, arg1);
});
editor.replaceRange = pendingInit(function(start, end, text) {
info.ace_replaceRange(start, end, text);
});
// calls to these functions ($$INCLUDE_...) are replaced when this file is processed
// and compressed, putting the compressed code from the named file directly into the
// source here.
var $$INCLUDE_CSS = function(fileName) {
return '<link rel="stylesheet" type="text/css" href="'+fileName+'"/>';
};
var $$INCLUDE_JS = function(fileName) {
return '\x3cscript type="text/javascript" src="'+fileName+'">\x3c/script>';
};
var $$INCLUDE_JS_DEV = $$INCLUDE_JS;
var $$INCLUDE_CSS_DEV = $$INCLUDE_CSS;
var $$INCLUDE_CSS_Q = function(fileName) {
return '\'<link rel="stylesheet" type="text/css" href="'+fileName+'"/>\'';
};
var $$INCLUDE_JS_Q = function(fileName) {
return '\'\\x3cscript type="text/javascript" src="'+fileName+'">\\x3c/script>\'';
};
var $$INCLUDE_JS_Q_DEV = $$INCLUDE_JS_Q;
var $$INCLUDE_CSS_Q_DEV = $$INCLUDE_CSS_Q;
editor.destroy = pendingInit(function() {
info.ace_dispose();
info.frame.parentNode.removeChild(info.frame);
delete ace2.registry[info.id];
info = null; // prevent IE 6 closure memory leaks
});
editor.init = function(containerId, initialCode, doneFunc) {
editor.importText(initialCode);
info.onEditorReady = function() {
loaded = true;
doActionsPendingInit();
doneFunc();
};
(function() {
var doctype = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" '+
'"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';
var iframeHTML = ["'"+doctype+"<html><head>'"];
plugins.callHook(
"aceInitInnerdocbodyHead", {iframeHTML:iframeHTML});
// these lines must conform to a specific format because they are passed by the build script:
//iframeHTML.push($$INCLUDE_CSS_Q("editor.css syntax.css inner.css"));
iframeHTML.push($$INCLUDE_CSS_Q("/static/css/editor.css"));
iframeHTML.push($$INCLUDE_CSS_Q("/static/css/syntax.cs"));
iframeHTML.push($$INCLUDE_CSS_Q("/static/css/inner.css"));
//iframeHTML.push(INCLUDE_JS_Q_DEV("ace2_common_dev.js"));
//iframeHTML.push(INCLUDE_JS_Q_DEV("profiler.js"));
//iframeHTML.push($$INCLUDE_JS_Q("ace2_common.js skiplist.js virtual_lines.js easysync2.js cssmanager.js colorutils.js undomodule.js contentcollector.js changesettracker.js linestylefilter.js domline.js"));
//iframeHTML.push($$INCLUDE_JS_Q("ace2_inner.js"));
iframeHTML.push($$INCLUDE_JS_Q("/static/js/ace2_common.js"));
iframeHTML.push($$INCLUDE_JS_Q("/static/js/skiplist.js"));
iframeHTML.push($$INCLUDE_JS_Q("/static/js/virtual_lines.js"));
iframeHTML.push($$INCLUDE_JS_Q("/static/js/easysync2.js"));
iframeHTML.push($$INCLUDE_JS_Q("/static/js/cssmanager.js"));
iframeHTML.push($$INCLUDE_JS_Q("/static/js/colorutils.js"));
iframeHTML.push($$INCLUDE_JS_Q("/static/js/undomodule.js"));
iframeHTML.push($$INCLUDE_JS_Q("/static/js/contentcollector.js"));
iframeHTML.push($$INCLUDE_JS_Q("/static/js/changesettracker.js"));
iframeHTML.push($$INCLUDE_JS_Q("/static/js/linestylefilter.js"));
iframeHTML.push($$INCLUDE_JS_Q("/static/js/domline.js"));
iframeHTML.push($$INCLUDE_JS_Q("/static/js/ace2_inner.js"));
iframeHTML.push('\'\\n<style type="text/css" title="dynamicsyntax"></style>\\n\'');
iframeHTML.push('\'</head><body id="innerdocbody" class="syntax" spellcheck="false">&nbsp;</body></html>\'');
var outerScript = 'editorId = "'+info.id+'"; editorInfo = parent.'+
thisFunctionsName+'.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; '+ // for IE
'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('+
iframeHTML.join('+')+'); doc.close(); '+
'}, 0); }';
var outerHTML = [doctype, '<html><head>',
$$INCLUDE_CSS("/static/css/editor.css"),
// bizarrely, in FF2, a file with no "external" dependencies won't finish loading properly
// (throbs busy while typing)
'<link rel="stylesheet" type="text/css" href="data:text/css,"/>',
'\x3cscript>', outerScript, '\x3c/script>',
'</head><body id="outerdocbody"><div id="sidediv"><!-- --></div><div id="linemetricsdiv">x</div><div id="overlaysdiv"><!-- --></div></body></html>'];
if (!Array.prototype.map) Array.prototype.map = function(fun) { //needed for IE
if (typeof fun != "function") throw new TypeError();
var len = this.length;
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;
};
var outerFrame = document.createElement("IFRAME");
outerFrame.frameBorder = 0; // for IE
info.frame = outerFrame;
document.getElementById(containerId).appendChild(outerFrame);
var editorDocument = outerFrame.contentWindow.document;
editorDocument.open();
editorDocument.write(outerHTML.join(''));
editorDocument.close();
})();
};
return editor;
}

30
static/js/ace.js.old Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,252 @@
/**
* 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.
*/
// requires: top
// requires: plugins
// requires: undefined
Ace2Editor.registry = { nextId: 1 };
function Ace2Editor() {
var thisFunctionsName = "Ace2Editor";
var ace2 = Ace2Editor;
var editor = {};
var info = { editor: editor, id: (ace2.registry.nextId++) };
var loaded = false;
var actionsPendingInit = [];
function pendingInit(func, optDoNow) {
return function() {
var that = this;
var args = arguments;
function action() {
func.apply(that, args);
}
if (optDoNow) {
optDoNow.apply(that, args);
}
if (loaded) {
action();
}
else {
actionsPendingInit.push(action);
}
};
}
function doActionsPendingInit() {
for(var i=0;i<actionsPendingInit.length;i++) {
actionsPendingInit[i]();
}
actionsPendingInit = [];
}
ace2.registry[info.id] = info;
editor.importText = pendingInit(function(newCode, undoable) {
info.ace_importText(newCode, undoable); });
editor.importAText = pendingInit(function(newCode, apoolJsonObj, undoable) {
info.ace_importAText(newCode, apoolJsonObj, undoable); });
editor.exportText = function() {
if (! loaded) return "(awaiting init)\n";
return info.ace_exportText();
};
editor.getFrame = function() { return info.frame || null; };
editor.focus = pendingInit(function() { info.ace_focus(); });
editor.setEditable = pendingInit(function(newVal) { info.ace_setEditable(newVal); });
editor.getFormattedCode = function() { return info.ace_getFormattedCode(); };
editor.setOnKeyPress = pendingInit(function (handler) { info.ace_setOnKeyPress(handler); });
editor.setOnKeyDown = pendingInit(function (handler) { info.ace_setOnKeyDown(handler); });
editor.setNotifyDirty = pendingInit(function (handler) { info.ace_setNotifyDirty(handler); });
editor.setProperty = pendingInit(function(key, value) { info.ace_setProperty(key, value); });
editor.getDebugProperty = function(prop) { return info.ace_getDebugProperty(prop); };
editor.setBaseText = pendingInit(function(txt) { info.ace_setBaseText(txt); });
editor.setBaseAttributedText = pendingInit(function(atxt, apoolJsonObj) {
info.ace_setBaseAttributedText(atxt, apoolJsonObj); });
editor.applyChangesToBase = pendingInit(function (changes, optAuthor,apoolJsonObj) {
info.ace_applyChangesToBase(changes, optAuthor, apoolJsonObj); });
// prepareUserChangeset:
// Returns null if no new changes or ACE not ready. Otherwise, bundles up all user changes
// to the latest base text into a Changeset, which is returned (as a string if encodeAsString).
// If this method returns a truthy value, then applyPreparedChangesetToBase can be called
// at some later point to consider these changes part of the base, after which prepareUserChangeset
// must be called again before applyPreparedChangesetToBase. Multiple consecutive calls
// to prepareUserChangeset will return an updated changeset that takes into account the
// latest user changes, and modify the changeset to be applied by applyPreparedChangesetToBase
// accordingly.
editor.prepareUserChangeset = function() {
if (! loaded) return null;
return info.ace_prepareUserChangeset();
};
editor.applyPreparedChangesetToBase = pendingInit(
function() { info.ace_applyPreparedChangesetToBase(); });
editor.setUserChangeNotificationCallback = pendingInit(function(callback) {
info.ace_setUserChangeNotificationCallback(callback);
});
editor.setAuthorInfo = pendingInit(function(author, authorInfo) {
info.ace_setAuthorInfo(author, authorInfo);
});
editor.setAuthorSelectionRange = pendingInit(function(author, start, end) {
info.ace_setAuthorSelectionRange(author, start, end);
});
editor.getUnhandledErrors = function() {
if (! loaded) return [];
// returns array of {error: <browser Error object>, time: +new Date()}
return info.ace_getUnhandledErrors();
};
editor.callWithAce = pendingInit(function(fn, callStack, normalize) {
return info.ace_callWithAce(fn, callStack, normalize);
});
editor.execCommand = pendingInit(function(cmd, arg1) {
info.ace_execCommand(cmd, arg1);
});
editor.replaceRange = pendingInit(function(start, end, text) {
info.ace_replaceRange(start, end, text);
});
// calls to these functions ($$INCLUDE_...) are replaced when this file is processed
// and compressed, putting the compressed code from the named file directly into the
// source here.
/*var $$INCLUDE_CSS = function(fileName) {
return '<link rel="stylesheet" type="text/css" href="'+fileName+'"/>';
};
var $$INCLUDE_JS = function(fileName) {
return '\x3cscript type="text/javascript" src="'+fileName+'">\x3c/script>';
};
var $$INCLUDE_JS_DEV = $$INCLUDE_JS;
var $$INCLUDE_CSS_DEV = $$INCLUDE_CSS;
var $$INCLUDE_CSS_Q = function(fileName) {
return '\'<link rel="stylesheet" type="text/css" href="'+fileName+'"/>\'';
};
var $$INCLUDE_JS_Q = function(fileName) {
return '\'\\x3cscript type="text/javascript" src="'+fileName+'">\\x3c/script>\'';
};
var $$INCLUDE_JS_Q_DEV = $$INCLUDE_JS_Q;
var $$INCLUDE_CSS_Q_DEV = $$INCLUDE_CSS_Q;*/
editor.destroy = pendingInit(function() {
info.ace_dispose();
info.frame.parentNode.removeChild(info.frame);
delete ace2.registry[info.id];
info = null; // prevent IE 6 closure memory leaks
});
editor.init = function(containerId, initialCode, doneFunc) {
editor.importText(initialCode);
info.onEditorReady = function() {
loaded = true;
doActionsPendingInit();
doneFunc();
};
(function() {
var doctype = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" '+
'"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';
var iframeHTML = ["'"+doctype+"<html><head>'"];
plugins.callHook(
"aceInitInnerdocbodyHead", {iframeHTML:iframeHTML});
// these lines must conform to a specific format because they are passed by the build script:
//iframeHTML.push($$INCLUDE_CSS_Q("editor.css syntax.css inner.css"));
iframeHTML.push('\'<link rel="stylesheet" type="text/css" href="/static/css/editor.css"/>\'');
iframeHTML.push('\'<link rel="stylesheet" type="text/css" href="/static/css/syntax.css"/>\'');
iframeHTML.push('\'<link rel="stylesheet" type="text/css" href="/static/css/inner.css"/>\'');
//iframeHTML.push(INCLUDE_JS_Q_DEV("ace2_common_dev.js"));
//iframeHTML.push(INCLUDE_JS_Q_DEV("profiler.js"));
// iframeHTML.push($$INCLUDE_JS_Q("ace2_common.js skiplist.js virtual_lines.js easysync2.js cssmanager.js colorutils.js undomodule.js contentcollector.js changesettracker.js linestylefilter.js domline.js"));
//iframeHTML.push($$INCLUDE_JS_Q("ace2_inner.js"));
iframeHTML.push('\'\\x3cscript type="text/javascript" src="/static/js/ace2_common.js">\\x3c/script>\'');
iframeHTML.push('\'\\x3cscript type="text/javascript" src="/static/js/skiplist.js">\\x3c/script>\'');
iframeHTML.push('\'\\x3cscript type="text/javascript" src="/static/js/virtual_lines.js">\\x3c/script>\'');
iframeHTML.push('\'\\x3cscript type="text/javascript" src="/static/js/easysync2.js">\\x3c/script>\'');
iframeHTML.push('\'\\x3cscript type="text/javascript" src="/static/js/cssmanager.js">\\x3c/script>\'');
iframeHTML.push('\'\\x3cscript type="text/javascript" src="/static/js/colorutils.js">\\x3c/script>\'');
iframeHTML.push('\'\\x3cscript type="text/javascript" src="/static/js/undomodule.js">\\x3c/script>\'');
iframeHTML.push('\'\\x3cscript type="text/javascript" src="/static/js/contentcollector.js">\\x3c/script>\'');
iframeHTML.push('\'\\x3cscript type="text/javascript" src="/static/js/changesettracker.js">\\x3c/script>\'');
iframeHTML.push('\'\\x3cscript type="text/javascript" src="/static/js/linestylefilter.js">\\x3c/script>\'');
iframeHTML.push('\'\\x3cscript type="text/javascript" src="/static/js/domline.js">\\x3c/script>\'');
iframeHTML.push('\'\\x3cscript type="text/javascript" src="/static/js/ace2_inner.js">\\x3c/script>\'');
iframeHTML.push('\'\\n<style type="text/css" title="dynamicsyntax"></style>\\n\'');
iframeHTML.push('\'</head><body id="innerdocbody" class="syntax" spellcheck="false">&nbsp;</body></html>\'');
var outerScript = 'editorId = "'+info.id+'"; editorInfo = parent.'+
thisFunctionsName+'.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; '+ // for IE
'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('+
iframeHTML.join('+')+'); doc.close(); '+
'}, 0); }';
var outerHTML = [doctype, '<html><head>',
'<style type="text/css" src="/static/css/editor.css"></style>',
// bizarrely, in FF2, a file with no "external" dependencies won't finish loading properly
// (throbs busy while typing)
'<link rel="stylesheet" type="text/css" href="data:text/css,"/>',
'\x3cscript>', outerScript, '\x3c/script>',
'</head><body id="outerdocbody"><div id="sidediv"><!-- --></div><div id="linemetricsdiv">x</div><div id="overlaysdiv"><!-- --></div></body></html>'];
if (!Array.prototype.map) Array.prototype.map = function(fun) { //needed for IE
if (typeof fun != "function") throw new TypeError();
var len = this.length;
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;
};
var outerFrame = document.createElement("IFRAME");
outerFrame.frameBorder = 0; // for IE
info.frame = outerFrame;
document.getElementById(containerId).appendChild(outerFrame);
var editorDocument = outerFrame.contentWindow.document;
editorDocument.open();
editorDocument.write(outerHTML.join(''));
editorDocument.close();
})();
};
return editor;
}

115
static/js/ace2_common.js Normal file
View file

@ -0,0 +1,115 @@
/**
* 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 isNodeText(node) {
return (node.nodeType == 3);
}
function object(o) {
var f = function() {};
f.prototype = o;
return new f();
}
function extend(obj, props) {
for(var p in props) {
obj[p] = props[p];
}
return obj;
}
function forEach(array, func) {
for(var i=0;i<array.length;i++) {
var result = func(array[i], i);
if (result) break;
}
}
function map(array, func) {
var result = [];
// must remain compatible with "arguments" pseudo-array
for(var i=0;i<array.length;i++) {
if (func) result.push(func(array[i], i));
else result.push(array[i]);
}
return result;
}
function filter(array, func) {
var result = [];
// must remain compatible with "arguments" pseudo-array
for(var i=0;i<array.length;i++) {
if (func(array[i], i)) result.push(array[i]);
}
return result;
}
function isArray(testObject) {
return testObject && typeof testObject === 'object' &&
!(testObject.propertyIsEnumerable('length')) &&
typeof testObject.length === 'number';
}
// Figure out what browser is being used (stolen from jquery 1.2.1)
var userAgent = navigator.userAgent.toLowerCase();
var browser = {
version: (userAgent.match(/.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/) || [])[1],
safari: /webkit/.test(userAgent),
opera: /opera/.test(userAgent),
msie: /msie/.test(userAgent) && !/opera/.test(userAgent),
mozilla: /mozilla/.test(userAgent) && !/(compatible|webkit)/.test(userAgent),
windows: /windows/.test(userAgent) // dgreensp
};
function getAssoc(obj, name) {
return obj["_magicdom_"+name];
}
function setAssoc(obj, name, value) {
// note that in IE designMode, properties of a node can get
// copied to new nodes that are spawned during editing; also,
// properties representable in HTML text can survive copy-and-paste
obj["_magicdom_"+name] = value;
}
// "func" is a function over 0..(numItems-1) that is monotonically
// "increasing" with index (false, then true). Finds the boundary
// between false and true, a number between 0 and numItems inclusive.
function binarySearch(numItems, func) {
if (numItems < 1) return 0;
if (func(0)) return 0;
if (! func(numItems-1)) return numItems;
var low = 0; // func(low) is always false
var high = numItems-1; // func(high) is always true
while ((high - low) > 1) {
var x = Math.floor((low+high)/2); // x != low, x != high
if (func(x)) high = x;
else low = x;
}
return high;
}
function binarySearchInfinite(expectedLength, func) {
var i = 0;
while (!func(i)) i += expectedLength;
return binarySearch(i, func);
}
function htmlPrettyEscape(str) {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/\r?\n/g, '\\n');
}

4588
static/js/ace2_inner.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,170 @@
/**
* 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 makeChangesetTracker(scheduler, apool, aceCallbacksProvider) {
// latest official text from server
var baseAText = Changeset.makeAText("\n");
// changes applied to baseText that have been submitted
var submittedChangeset = null;
// changes applied to submittedChangeset since it was prepared
var userChangeset = Changeset.identity(1);
// is the changesetTracker enabled
var tracking = false;
// stack state flag so that when we change the rep we don't
// handle the notification recursively. When setting, always
// unset in a "finally" block. When set to true, the setter
// takes change of userChangeset.
var applyingNonUserChanges = false;
var changeCallback = null;
var changeCallbackTimeout = null;
function setChangeCallbackTimeout() {
// can call this multiple times per call-stack, because
// we only schedule a call to changeCallback if it exists
// and if there isn't a timeout already scheduled.
if (changeCallback && changeCallbackTimeout === null) {
changeCallbackTimeout = scheduler.setTimeout(function() {
try {
changeCallback();
}
finally {
changeCallbackTimeout = null;
}
}, 0);
}
}
var self;
return self = {
isTracking: function() { return tracking; },
setBaseText: function(text) {
self.setBaseAttributedText(Changeset.makeAText(text), null);
},
setBaseAttributedText: function(atext, apoolJsonObj) {
aceCallbacksProvider.withCallbacks("setBaseText", function(callbacks) {
tracking = true;
baseAText = Changeset.cloneAText(atext);
if (apoolJsonObj) {
var wireApool = (new AttribPool()).fromJsonable(apoolJsonObj);
baseAText.attribs = Changeset.moveOpsToNewPool(baseAText.attribs, wireApool, apool);
}
submittedChangeset = null;
userChangeset = Changeset.identity(atext.text.length);
applyingNonUserChanges = true;
try {
callbacks.setDocumentAttributedText(atext);
}
finally {
applyingNonUserChanges = false;
}
});
},
composeUserChangeset: function(c) {
if (! tracking) return;
if (applyingNonUserChanges) return;
if (Changeset.isIdentity(c)) return;
userChangeset = Changeset.compose(userChangeset, c, apool);
setChangeCallbackTimeout();
},
applyChangesToBase: function (c, optAuthor, apoolJsonObj) {
if (! tracking) return;
aceCallbacksProvider.withCallbacks("applyChangesToBase", function(callbacks) {
if (apoolJsonObj) {
var wireApool = (new AttribPool()).fromJsonable(apoolJsonObj);
c = Changeset.moveOpsToNewPool(c, wireApool, apool);
}
baseAText = Changeset.applyToAText(c, baseAText, apool);
var c2 = c;
if (submittedChangeset) {
var oldSubmittedChangeset = submittedChangeset;
submittedChangeset = Changeset.follow(c, oldSubmittedChangeset, false, apool);
c2 = Changeset.follow(oldSubmittedChangeset, c, true, apool);
}
var preferInsertingAfterUserChanges = true;
var oldUserChangeset = userChangeset;
userChangeset = Changeset.follow(c2, oldUserChangeset, preferInsertingAfterUserChanges, apool);
var postChange =
Changeset.follow(oldUserChangeset, c2, ! preferInsertingAfterUserChanges, apool);
var preferInsertionAfterCaret = true; //(optAuthor && optAuthor > thisAuthor);
applyingNonUserChanges = true;
try {
callbacks.applyChangesetToDocument(postChange, preferInsertionAfterCaret);
}
finally {
applyingNonUserChanges = false;
}
});
},
prepareUserChangeset: function() {
// If there are user changes to submit, 'changeset' will be the
// changeset, else it will be null.
var toSubmit;
if (submittedChangeset) {
// submission must have been canceled, prepare new changeset
// that includes old submittedChangeset
toSubmit = Changeset.compose(submittedChangeset, userChangeset, apool);
}
else {
if (Changeset.isIdentity(userChangeset)) toSubmit = null;
else toSubmit = userChangeset;
}
var cs = null;
if (toSubmit) {
submittedChangeset = toSubmit;
userChangeset = Changeset.identity(Changeset.newLen(toSubmit));
cs = toSubmit;
}
var wireApool = null;
if (cs) {
var forWire = Changeset.prepareForWire(cs, apool);
wireApool = forWire.pool.toJsonable();
cs = forWire.translated;
}
var data = { changeset: cs, apool: wireApool };
return data;
},
applyPreparedChangesetToBase: function() {
if (! submittedChangeset) {
// violation of protocol; use prepareUserChangeset first
throw new Error("applySubmittedChangesToBase: no submitted changes to apply");
}
//bumpDebug("applying committed changeset: "+submittedChangeset.encodeToString(false));
baseAText = Changeset.applyToAText(submittedChangeset, baseAText, apool);
submittedChangeset = null;
},
setUserChangeNotificationCallback: function (callback) {
changeCallback = callback;
},
hasUncommittedChanges: function() {
return !!(submittedChangeset || (! Changeset.isIdentity(userChangeset)));
}
};
}

666
static/js/collab_client.js Normal file
View file

@ -0,0 +1,666 @@
/**
* 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);*/
socket.disconnect();
}
});
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()
{
//oldSocketId = String(Math.floor(Math.random()*1e12));
//socketId = String(Math.floor(Math.random()*1e12));
/*socket = new io.Socket();
socket.connect();*/
//socket.on('connect', 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();
initialStartConnectTime = +new Date();
// });
/*socket.on('message', function(obj){
if(window.console)
console.log(obj);
handleMessageFromServer(obj);
});*/
socket.on('disconnect', function(obj){
handleSocketClosed(true);
});
/*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}));
socket.send(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 = evt;
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<deferredActions.length;i++) {
var a = deferredActions[i];
if ((!tag) || (tag == a.tag)) {
a();
}
else {
newArray.push(a);
}
}
deferredActions = newArray;
}
function sendClientMessage(msg) {
sendMessage({ type: "CLIENT_MESSAGE", payload: msg });
}
function getCurrentRevisionNumber() {
return rev;
}
function getDiagnosticInfo() {
var maxCaughtErrors = 3;
var maxAceErrors = 3;
var maxDebugMessages = 50;
var longStringCutoff = 500;
function trunc(str) {
return String(str).substring(0, longStringCutoff);
}
var info = { errors: {length: 0} };
function addError(e, catcher, time) {
var error = {catcher:catcher};
if (time) error.time = time;
// a little over-cautious?
try { if (e.description) error.description = e.description; } catch (x) {}
try { if (e.fileName) error.fileName = e.fileName; } catch (x) {}
try { if (e.lineNumber) error.lineNumber = e.lineNumber; } catch (x) {}
try { if (e.message) error.message = e.message; } catch (x) {}
try { if (e.name) error.name = e.name; } catch (x) {}
try { if (e.number) error.number = e.number; } catch (x) {}
try { if (e.stack) error.stack = trunc(e.stack); } catch (x) {}
info.errors[info.errors.length] = error;
info.errors.length++;
}
for(var i=0; ((i<caughtErrors.length) && (i<maxCaughtErrors)); i++) {
addError(caughtErrors[i], caughtErrorCatchers[i], caughtErrorTimes[i]);
}
if (editor) {
var aceErrors = editor.getUnhandledErrors();
for(var i=0; ((i<aceErrors.length) && (i<maxAceErrors)) ;i++) {
var errorRecord = aceErrors[i];
addError(errorRecord.error, "ACE", errorRecord.time);
}
}
info.time = +new Date();
info.collabState = state;
info.channelState = channelState;
info.lastCommitTime = lastCommitTime;
info.numSocketReconnects = reconnectTimes.length;
info.userId = userId;
info.currentRev = rev;
info.participants = (function() {
var pp = [];
for(var u in userSet) {
pp.push(u);
}
return pp.join(',');
})();
if (debugMessages.length > maxDebugMessages) {
debugMessages = debugMessages.slice(debugMessages.length-maxDebugMessages,
debugMessages.length);
}
info.debugMessages = {length: 0};
for(var i=0;i<debugMessages.length;i++) {
info.debugMessages[i] = trunc(debugMessages[i]);
info.debugMessages.length++;
}
return info;
}
function getMissedChanges() {
var obj = {};
obj.userInfo = userSet[userId];
obj.baseRev = rev;
if (state == "COMMITTING" && stateMessage) {
obj.committedChangeset = stateMessage.changeset;
obj.committedChangesetAPool = stateMessage.apool;
obj.committedChangesetSocketId = stateMessageSocketId;
editor.applyPreparedChangesetToBase();
}
var userChangesData = editor.prepareUserChangeset();
if (userChangesData.changeset) {
obj.furtherChangeset = userChangesData.changeset;
obj.furtherChangesetAPool = userChangesData.apool;
}
return obj;
}
function setStateIdle() {
state = "IDLE";
callbacks.onInternalAction("newlyIdle");
schedulePerhapsCallIdleFuncs();
}
function callWhenNotCommitting(func) {
idleFuncs.push(func);
schedulePerhapsCallIdleFuncs();
}
var idleFuncs = [];
function schedulePerhapsCallIdleFuncs() {
setTimeout(function() {
if (state == "IDLE") {
while (idleFuncs.length > 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);
}
}
}
}

92
static/js/colorutils.js Normal file
View file

@ -0,0 +1,92 @@
// DO NOT EDIT THIS FILE, edit infrastructure/ace/www/colorutils.js
// THIS FILE IS ALSO SERVED AS CLIENT-SIDE 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])];
}

View file

@ -0,0 +1,520 @@
// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.contentcollector
// %APPJET%: import("etherpad.collab.ace.easysync2.Changeset");
// %APPJET%: import("etherpad.admin.plugins");
/**
* 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 _MAX_LIST_LEVEL = 8;
function sanitizeUnicode(s) {
return s.replace(/[\uffff\ufffe\ufeff\ufdd0-\ufdef\ud800-\udfff]/g, '?');
}
function makeContentCollector(collectStyles, browser, apool, domInterface,
className2Author) {
browser = browser || {};
var plugins_;
if (typeof(plugins)!='undefined') {
plugins_ = plugins;
} else {
plugins_ = parent.parent.plugins;
}
var dom = domInterface || {
isNodeText: function(n) {
return (n.nodeType == 3);
},
nodeTagName: function(n) {
return n.tagName;
},
nodeValue: function(n) {
return n.nodeValue;
},
nodeNumChildren: function(n) {
return n.childNodes.length;
},
nodeChild: function(n, i) {
return n.childNodes.item(i);
},
nodeProp: function(n, p) {
return n[p];
},
nodeAttr: function(n, a) {
return n.getAttribute(a);
},
optNodeInnerHTML: function(n) {
return n.innerHTML;
}
};
var _blockElems = { "div":1, "p":1, "pre":1, "li":1 };
function isBlockElement(n) {
return !!_blockElems[(dom.nodeTagName(n) || "").toLowerCase()];
}
function textify(str) {
return sanitizeUnicode(
str.replace(/[\n\r ]/g, ' ').replace(/\xa0/g, ' ').replace(/\t/g, ' '));
}
function getAssoc(node, name) {
return dom.nodeProp(node, "_magicdom_"+name);
}
var lines = (function() {
var textArray = [];
var attribsArray = [];
var attribsBuilder = null;
var op = Changeset.newOp('+');
var self = {
length: function() { return textArray.length; },
atColumnZero: function() {
return textArray[textArray.length-1] === "";
},
startNew: function() {
textArray.push("");
self.flush(true);
attribsBuilder = Changeset.smartOpAssembler();
},
textOfLine: function(i) { return textArray[i]; },
appendText: function(txt, attrString) {
textArray[textArray.length-1] += txt;
//dmesg(txt+" / "+attrString);
op.attribs = attrString;
op.chars = txt.length;
attribsBuilder.append(op);
},
textLines: function() { return textArray.slice(); },
attribLines: function() { return attribsArray; },
// call flush only when you're done
flush: function(withNewline) {
if (attribsBuilder) {
attribsArray.push(attribsBuilder.toString());
attribsBuilder = null;
}
}
};
self.startNew();
return self;
}());
var cc = {};
function _ensureColumnZero(state) {
if (! lines.atColumnZero()) {
cc.startNewLine(state);
}
}
var selection, startPoint, endPoint;
var selStart = [-1,-1], selEnd = [-1,-1];
var blockElems = { "div":1, "p":1, "pre":1 };
function _isEmpty(node, state) {
// consider clean blank lines pasted in IE to be empty
if (dom.nodeNumChildren(node) == 0) return true;
if (dom.nodeNumChildren(node) == 1 &&
getAssoc(node, "shouldBeEmpty") && dom.optNodeInnerHTML(node) == "&nbsp;"
&& ! getAssoc(node, "unpasted")) {
if (state) {
var child = dom.nodeChild(node, 0);
_reachPoint(child, 0, state);
_reachPoint(child, 1, state);
}
return true;
}
return false;
}
function _pointHere(charsAfter, state) {
var ln = lines.length()-1;
var chr = lines.textOfLine(ln).length;
if (chr == 0 && state.listType && state.listType != 'none') {
chr += 1; // listMarker
}
chr += charsAfter;
return [ln, chr];
}
function _reachBlockPoint(nd, idx, state) {
if (! dom.isNodeText(nd)) _reachPoint(nd, idx, state);
}
function _reachPoint(nd, idx, state) {
if (startPoint && nd == startPoint.node && startPoint.index == idx) {
selStart = _pointHere(0, state);
}
if (endPoint && nd == endPoint.node && endPoint.index == idx) {
selEnd = _pointHere(0, state);
}
}
cc.incrementFlag = function(state, flagName) {
state.flags[flagName] = (state.flags[flagName] || 0)+1;
}
cc.decrementFlag = function(state, flagName) {
state.flags[flagName]--;
}
cc.incrementAttrib = function(state, attribName) {
if (! state.attribs[attribName]) {
state.attribs[attribName] = 1;
}
else {
state.attribs[attribName]++;
}
_recalcAttribString(state);
}
cc.decrementAttrib = function(state, attribName) {
state.attribs[attribName]--;
_recalcAttribString(state);
}
function _enterList(state, listType) {
var oldListType = state.listType;
state.listLevel = (state.listLevel || 0)+1;
if (listType != 'none') {
state.listNesting = (state.listNesting || 0)+1;
}
state.listType = listType;
_recalcAttribString(state);
return oldListType;
}
function _exitList(state, oldListType) {
state.listLevel--;
if (state.listType != 'none') {
state.listNesting--;
}
state.listType = oldListType;
_recalcAttribString(state);
}
function _enterAuthor(state, author) {
var oldAuthor = state.author;
state.authorLevel = (state.authorLevel || 0)+1;
state.author = author;
_recalcAttribString(state);
return oldAuthor;
}
function _exitAuthor(state, oldAuthor) {
state.authorLevel--;
state.author = oldAuthor;
_recalcAttribString(state);
}
function _recalcAttribString(state) {
var lst = [];
for(var a in state.attribs) {
if (state.attribs[a]) {
lst.push([a,'true']);
}
}
if (state.authorLevel > 0) {
var authorAttrib = ['author', state.author];
if (apool.putAttrib(authorAttrib, true) >= 0) {
// require that author already be in pool
// (don't add authors from other documents, etc.)
lst.push(authorAttrib);
}
}
state.attribString = Changeset.makeAttribsString('+', lst, apool);
}
function _produceListMarker(state) {
lines.appendText('*', Changeset.makeAttribsString(
'+', [['list', state.listType],
['insertorder', 'first']],
apool));
}
cc.startNewLine = function(state) {
if (state) {
var atBeginningOfLine = lines.textOfLine(lines.length()-1).length == 0;
if (atBeginningOfLine && state.listType && state.listType != 'none') {
_produceListMarker(state);
}
}
lines.startNew();
}
cc.notifySelection = function (sel) {
if (sel) {
selection = sel;
startPoint = selection.startPoint;
endPoint = selection.endPoint;
}
};
cc.doAttrib = function(state, na) {
state.localAttribs = (state.localAttribs || []);
state.localAttribs.push(na);
cc.incrementAttrib(state, na);
};
cc.collectContent = function (node, state) {
if (! state) {
state = {flags: {/*name -> nesting counter*/},
localAttribs: null,
attribs: {/*name -> nesting counter*/},
attribString: ''};
}
var localAttribs = state.localAttribs;
state.localAttribs = null;
var isBlock = isBlockElement(node);
var isEmpty = _isEmpty(node, state);
if (isBlock) _ensureColumnZero(state);
var startLine = lines.length()-1;
_reachBlockPoint(node, 0, state);
if (dom.isNodeText(node)) {
var txt = dom.nodeValue(node);
var rest = '';
var x = 0; // offset into original text
if (txt.length == 0) {
if (startPoint && node == startPoint.node) {
selStart = _pointHere(0, state);
}
if (endPoint && node == endPoint.node) {
selEnd = _pointHere(0, state);
}
}
while (txt.length > 0) {
var consumed = 0;
if (state.flags.preMode) {
var firstLine = txt.split('\n',1)[0];
consumed = firstLine.length+1;
rest = txt.substring(consumed);
txt = firstLine;
}
else { /* will only run this loop body once */ }
if (startPoint && node == startPoint.node &&
startPoint.index-x <= txt.length) {
selStart = _pointHere(startPoint.index-x, state);
}
if (endPoint && node == endPoint.node &&
endPoint.index-x <= txt.length) {
selEnd = _pointHere(endPoint.index-x, state);
}
var txt2 = txt;
if ((! state.flags.preMode) && /^[\r\n]*$/.exec(txt)) {
// prevents textnodes containing just "\n" from being significant
// in safari when pasting text, now that we convert them to
// spaces instead of removing them, because in other cases
// removing "\n" from pasted HTML will collapse words together.
txt2 = "";
}
var atBeginningOfLine = lines.textOfLine(lines.length()-1).length == 0;
if (atBeginningOfLine) {
// newlines in the source mustn't become spaces at beginning of line box
txt2 = txt2.replace(/^\n*/, '');
}
if (atBeginningOfLine && state.listType && state.listType != 'none') {
_produceListMarker(state);
}
lines.appendText(textify(txt2), state.attribString);
x += consumed;
txt = rest;
if (txt.length > 0) {
cc.startNewLine(state);
}
}
}
else {
var tname = (dom.nodeTagName(node) || "").toLowerCase();
if (tname == "br") {
cc.startNewLine(state);
}
else if (tname == "script" || tname == "style") {
// ignore
}
else if (! isEmpty) {
var styl = dom.nodeAttr(node, "style");
var cls = dom.nodeProp(node, "className");
var isPre = (tname == "pre");
if ((! isPre) && browser.safari) {
isPre = (styl && /\bwhite-space:\s*pre\b/i.exec(styl));
}
if (isPre) cc.incrementFlag(state, 'preMode');
var oldListTypeOrNull = null;
var oldAuthorOrNull = null;
if (collectStyles) {
plugins_.callHook('collectContentPre', {cc: cc, state:state, tname:tname, styl:styl, cls:cls});
if (tname == "b" || (styl && /\bfont-weight:\s*bold\b/i.exec(styl)) ||
tname == "strong") {
cc.doAttrib(state, "bold");
}
if (tname == "i" || (styl && /\bfont-style:\s*italic\b/i.exec(styl)) ||
tname == "em") {
cc.doAttrib(state, "italic");
}
if (tname == "u" || (styl && /\btext-decoration:\s*underline\b/i.exec(styl)) ||
tname == "ins") {
cc.doAttrib(state, "underline");
}
if (tname == "s" || (styl && /\btext-decoration:\s*line-through\b/i.exec(styl)) ||
tname == "del") {
cc.doAttrib(state, "strikethrough");
}
if (tname == "ul") {
var type;
var rr = cls && /(?:^| )list-(bullet[12345678])\b/.exec(cls);
type = rr && rr[1] || "bullet"+
String(Math.min(_MAX_LIST_LEVEL, (state.listNesting||0)+1));
oldListTypeOrNull = (_enterList(state, type) || 'none');
}
else if ((tname == "div" || tname == "p") && cls &&
cls.match(/(?:^| )ace-line\b/)) {
oldListTypeOrNull = (_enterList(state, type) || 'none');
}
if (className2Author && cls) {
var classes = cls.match(/\S+/g);
if (classes && classes.length > 0) {
for(var i=0;i<classes.length;i++) {
var c = classes[i];
var a = className2Author(c);
if (a) {
oldAuthorOrNull = (_enterAuthor(state, a) || 'none');
break;
}
}
}
}
}
var nc = dom.nodeNumChildren(node);
for(var i=0;i<nc;i++) {
var c = dom.nodeChild(node, i);
cc.collectContent(c, state);
}
if (collectStyles) {
plugins_.callHook('collectContentPost', {cc: cc, state:state, tname:tname, styl:styl, cls:cls});
}
if (isPre) cc.decrementFlag(state, 'preMode');
if (state.localAttribs) {
for(var i=0;i<state.localAttribs.length;i++) {
cc.decrementAttrib(state, state.localAttribs[i]);
}
}
if (oldListTypeOrNull) {
_exitList(state, oldListTypeOrNull);
}
if (oldAuthorOrNull) {
_exitAuthor(state, oldAuthorOrNull);
}
}
}
if (! browser.msie) {
_reachBlockPoint(node, 1, state);
}
if (isBlock) {
if (lines.length()-1 == startLine) {
cc.startNewLine(state);
}
else {
_ensureColumnZero(state);
}
}
if (browser.msie) {
// in IE, a point immediately after a DIV appears on the next line
_reachBlockPoint(node, 1, state);
}
state.localAttribs = localAttribs;
};
// can pass a falsy value for end of doc
cc.notifyNextNode = function (node) {
// an "empty block" won't end a line; this addresses an issue in IE with
// typing into a blank line at the end of the document. typed text
// goes into the body, and the empty line div still looks clean.
// it is incorporated as dirty by the rule that a dirty region has
// to end a line.
if ((!node) || (isBlockElement(node) && !_isEmpty(node))) {
_ensureColumnZero(null);
}
};
// each returns [line, char] or [-1,-1]
var getSelectionStart = function() { return selStart; };
var getSelectionEnd = function() { return selEnd; };
// returns array of strings for lines found, last entry will be "" if
// last line is complete (i.e. if a following span should be on a new line).
// can be called at any point
cc.getLines = function() { return lines.textLines(); };
cc.finish = function() {
lines.flush();
var lineAttribs = lines.attribLines();
var lineStrings = cc.getLines();
lineStrings.length--;
lineAttribs.length--;
var ss = getSelectionStart();
var se = getSelectionEnd();
function fixLongLines() {
// design mode does not deal with with really long lines!
var lineLimit = 2000; // chars
var buffer = 10; // chars allowed over before wrapping
var linesWrapped = 0;
var numLinesAfter = 0;
for(var i=lineStrings.length-1; i>=0; i--) {
var oldString = lineStrings[i];
var oldAttribString = lineAttribs[i];
if (oldString.length > lineLimit+buffer) {
var newStrings = [];
var newAttribStrings = [];
while (oldString.length > lineLimit) {
//var semiloc = oldString.lastIndexOf(';', lineLimit-1);
//var lengthToTake = (semiloc >= 0 ? (semiloc+1) : lineLimit);
lengthToTake = lineLimit;
newStrings.push(oldString.substring(0, lengthToTake));
oldString = oldString.substring(lengthToTake);
newAttribStrings.push(Changeset.subattribution(oldAttribString,
0, lengthToTake));
oldAttribString = Changeset.subattribution(oldAttribString,
lengthToTake);
}
if (oldString.length > 0) {
newStrings.push(oldString);
newAttribStrings.push(oldAttribString);
}
function fixLineNumber(lineChar) {
if (lineChar[0] < 0) return;
var n = lineChar[0];
var c = lineChar[1];
if (n > i) {
n += (newStrings.length-1);
}
else if (n == i) {
var a = 0;
while (c > newStrings[a].length) {
c -= newStrings[a].length;
a++;
}
n += a;
}
lineChar[0] = n;
lineChar[1] = c;
}
fixLineNumber(ss);
fixLineNumber(se);
linesWrapped++;
numLinesAfter += newStrings.length;
newStrings.unshift(i, 1);
lineStrings.splice.apply(lineStrings, newStrings);
newAttribStrings.unshift(i, 1);
lineAttribs.splice.apply(lineAttribs, newAttribStrings);
}
}
return {linesWrapped:linesWrapped, numLinesAfter:numLinesAfter};
}
var wrapData = fixLongLines();
return { selStart: ss, selEnd: se, linesWrapped: wrapData.linesWrapped,
numLinesAfter: wrapData.numLinesAfter,
lines: lineStrings, lineAttribs: lineAttribs };
}
return cc;
}

88
static/js/cssmanager.js Normal file
View file

@ -0,0 +1,88 @@
/**
* 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<allSheets.length;i++) {
var s = allSheets[i];
if (s.title == title) {
return s;
}
}
return null;
}
/*function getSheetTagByTitle(title) {
var allStyleTags = document.getElementsByTagName("style");
for(var i=0;i<allStyleTags.length;i++) {
var t = allStyleTags[i];
if (t.title == title) {
return t;
}
}
return null;
}*/
var browserSheet = getSheetByTitle(emptyStylesheetTitle);
//var browserTag = getSheetTagByTitle(emptyStylesheetTitle);
function browserRules() { return (browserSheet.cssRules || browserSheet.rules); }
function browserDeleteRule(i) {
if (browserSheet.deleteRule) browserSheet.deleteRule(i);
else browserSheet.removeRule(i);
}
function browserInsertRule(i, selector) {
if (browserSheet.insertRule) browserSheet.insertRule(selector+' {}', i);
else browserSheet.addRule(selector, null, i);
}
var selectorList = [];
function indexOfSelector(selector) {
for(var i=0;i<selectorList.length;i++) {
if (selectorList[i] == selector) {
return i;
}
}
return -1;
}
function selectorStyle(selector) {
var i = indexOfSelector(selector);
if (i < 0) {
// add selector
browserInsertRule(0, selector);
selectorList.splice(0, 0, selector);
i = 0;
}
return browserRules().item(i).style;
}
function removeSelectorStyle(selector) {
var i = indexOfSelector(selector);
if (i >= 0) {
browserDeleteRule(i);
selectorList.splice(i, 1);
}
}
return {selectorStyle:selectorStyle, removeSelectorStyle:removeSelectorStyle,
info: function() {
return selectorList.length+":"+browserRules().length;
}};
}

232
static/js/domline.js Normal file
View file

@ -0,0 +1,232 @@
// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.domline
// %APPJET%: import("etherpad.admin.plugins");
/**
* 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.
*/
// requires: top
// requires: plugins
// requires: undefined
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 = '<ul class="list-'+listType+'"><li>';
postHtml = '</li></ul>';
}
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;
});
}
var extraOpenTags = "";
var extraCloseTags = "";
var plugins_;
if (typeof(plugins)!='undefined') {
plugins_ = plugins;
} else {
plugins_ = parent.parent.plugins;
}
plugins_.callHook(
"aceCreateDomLine", {domline:domline, cls:cls}
).map(function (modifier) {
cls = modifier.cls;
extraOpenTags = extraOpenTags+modifier.extraOpenTags;
extraCloseTags = modifier.extraCloseTags+extraCloseTags;
});
if ((! txt) && cls) {
lineClass = domline.addToLineClass(lineClass, cls);
}
else if (txt) {
if (href) {
extraOpenTags = extraOpenTags+'<a href="'+
href.replace(/\"/g, '&quot;')+'">';
extraCloseTags = '</a>'+extraCloseTags;
}
if (simpleTags) {
simpleTags.sort();
extraOpenTags = extraOpenTags+'<'+simpleTags.join('><')+'>';
simpleTags.reverse();
extraCloseTags = '</'+simpleTags.join('></')+'>'+extraCloseTags;
}
html.push('<span class="',cls||'','">',extraOpenTags,
perTextNodeProcess(domline.escapeHTML(txt)),
extraCloseTags,'</span>');
}
};
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 += '&nbsp;';
}
else if (! browser.msie) {
newHTML += '<br/>';
}
}
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 = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&#34;',
"'": '&#39;'
};
}
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, '&nbsp;');
}
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] = '&nbsp;';
endOfLine = false;
beforeSpace = true;
}
else if (p.charAt(0) != "<") {
endOfLine = false;
beforeSpace = false;
}
}
// beginning of line is nbsp
for(var i=0;i<parts.length;i++) {
var p = parts[i];
if (p == " ") {
parts[i] = '&nbsp;';
break;
}
else if (p.charAt(0) != "<") {
break;
}
}
}
else {
for(var i=0;i<parts.length;i++) {
var p = parts[i];
if (p == " ") {
parts[i] = '&nbsp;';
}
}
}
return parts.join('');
};

151
static/js/draggable.js Normal file
View file

@ -0,0 +1,151 @@
/**
* 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 makeDraggable(jqueryNodes, eventHandler) {
jqueryNodes.each(function() {
var node = $(this);
var state = {};
var inDrag = false;
function dragStart(evt) {
if (inDrag) {
return;
}
inDrag = true;
if (eventHandler('dragstart', evt, state) !== false) {
$(document).bind('mousemove', dragUpdate);
$(document).bind('mouseup', dragEnd);
}
evt.preventDefault();
return false;
}
function dragUpdate(evt) {
if (! inDrag) {
return;
}
eventHandler('dragupdate', evt, state);
evt.preventDefault();
return false;
}
function dragEnd(evt) {
if (! inDrag) {
return;
}
inDrag = false;
try {
eventHandler('dragend', evt, state);
}
finally {
$(document).unbind('mousemove', dragUpdate);
$(document).unbind('mouseup', dragEnd);
evt.preventDefault();
}
return false;
}
node.bind('mousedown', dragStart);
});
}
function makeResizableVPane(top, sep, bottom, minTop, minBottom) {
if (minTop === undefined) minTop = 0;
if (minBottom === undefined) minBottom = 0;
makeDraggable($(sep), function(eType, evt, state) {
if (eType == 'dragstart') {
state.startY = evt.pageY;
state.topHeight = $(top).height();
state.bottomHeight = $(bottom).height();
state.minTop = minTop;
state.maxTop = (state.topHeight + state.bottomHeight) - minBottom;
}
else if (eType == 'dragupdate') {
var change = evt.pageY - state.startY;
var topHeight = state.topHeight + change;
if (topHeight < state.minTop) { topHeight = state.minTop; }
if (topHeight > state.maxTop) { topHeight = state.maxTop; }
change = topHeight - state.topHeight;
var bottomHeight = state.bottomHeight - change;
var sepHeight = $(sep).height();
var totalHeight = topHeight + sepHeight + bottomHeight;
topHeight = 100.0 * topHeight / totalHeight;
sepHeight = 100.0 * sepHeight / totalHeight;
bottomHeight = 100.0 * bottomHeight / totalHeight;
$(top).css('bottom', 'auto');
$(top).css('height', topHeight + "%");
$(sep).css('top', topHeight + "%");
$(bottom).css('top', (topHeight + sepHeight) + '%');
$(bottom).css('height', 'auto');
}
});
}
function makeResizableHPane(left, sep, right, minLeft, minRight, sepWidth, sepOffset) {
if (minLeft === undefined) minLeft = 0;
if (minRight === undefined) minRight = 0;
makeDraggable($(sep), function(eType, evt, state) {
if (eType == 'dragstart') {
state.startX = evt.pageX;
state.leftWidth = $(left).width();
state.rightWidth = $(right).width();
state.minLeft = minLeft;
state.maxLeft = (state.leftWidth + state.rightWidth) - minRight;
} else if (eType == 'dragend' || eType == 'dragupdate') {
var change = evt.pageX - state.startX;
var leftWidth = state.leftWidth + change;
if (leftWidth < state.minLeft) { leftWidth = state.minLeft; }
if (leftWidth > state.maxLeft) { leftWidth = state.maxLeft; }
change = leftWidth - state.leftWidth;
var rightWidth = state.rightWidth - change;
newSepWidth = sepWidth;
if (newSepWidth == undefined)
newSepWidth = $(sep).width();
newSepOffset = sepOffset;
if (newSepOffset == undefined)
newSepOffset = 0;
if (change == 0) {
if (rightWidth != minRight || state.lastRightWidth == undefined) {
state.lastRightWidth = rightWidth;
rightWidth = minRight;
} else {
rightWidth = state.lastRightWidth;
state.lastRightWidth = minRight;
}
change = state.rightWidth - rightWidth;
leftWidth = change + state.leftWidth;
}
var totalWidth = leftWidth + newSepWidth + rightWidth;
leftWidth = 100.0 * leftWidth / totalWidth;
newSepWidth = 100.0 * newSepWidth / totalWidth;
newSepOffset = 100.0 * newSepOffset / totalWidth;
rightWidth = 100.0 * rightWidth / totalWidth;
$(left).css('right', 'auto');
$(left).css('width', leftWidth + "%");
$(sep).css('left', (leftWidth + newSepOffset) + "%");
$(right).css('left', (leftWidth + newSepWidth) + '%');
$(right).css('width', 'auto');
}
});
}

1968
static/js/easysync2.js Normal file

File diff suppressed because it is too large Load diff

4376
static/js/jquery-1.3.2.js vendored Normal file

File diff suppressed because it is too large Load diff

480
static/js/json2.js Normal file
View file

@ -0,0 +1,480 @@
/*
http://www.JSON.org/json2.js
2011-02-23
Public Domain.
NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
See http://www.JSON.org/js.html
This code should be minified before deployment.
See http://javascript.crockford.com/jsmin.html
USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
NOT CONTROL.
This file creates a global JSON object containing two methods: stringify
and parse.
JSON.stringify(value, replacer, space)
value any JavaScript value, usually an object or array.
replacer an optional parameter that determines how object
values are stringified for objects. It can be a
function or an array of strings.
space an optional parameter that specifies the indentation
of nested structures. If it is omitted, the text will
be packed without extra whitespace. If it is a number,
it will specify the number of spaces to indent at each
level. If it is a string (such as '\t' or '&nbsp;'),
it contains the characters used to indent at each level.
This method produces a JSON text from a JavaScript value.
When an object value is found, if the object contains a toJSON
method, its toJSON method will be called and the result will be
stringified. A toJSON method does not serialize: it returns the
value represented by the name/value pair that should be serialized,
or undefined if nothing should be serialized. The toJSON method
will be passed the key associated with the value, and this will be
bound to the value
For example, this would serialize Dates as ISO strings.
Date.prototype.toJSON = function (key) {
function f(n) {
// Format integers to have at least two digits.
return n < 10 ? '0' + n : n;
}
return this.getUTCFullYear() + '-' +
f(this.getUTCMonth() + 1) + '-' +
f(this.getUTCDate()) + 'T' +
f(this.getUTCHours()) + ':' +
f(this.getUTCMinutes()) + ':' +
f(this.getUTCSeconds()) + 'Z';
};
You can provide an optional replacer method. It will be passed the
key and value of each member, with this bound to the containing
object. The value that is returned from your method will be
serialized. If your method returns undefined, then the member will
be excluded from the serialization.
If the replacer parameter is an array of strings, then it will be
used to select the members to be serialized. It filters the results
such that only members with keys listed in the replacer array are
stringified.
Values that do not have JSON representations, such as undefined or
functions, will not be serialized. Such values in objects will be
dropped; in arrays they will be replaced with null. You can use
a replacer function to replace those with JSON values.
JSON.stringify(undefined) returns undefined.
The optional space parameter produces a stringification of the
value that is filled with line breaks and indentation to make it
easier to read.
If the space parameter is a non-empty string, then that string will
be used for indentation. If the space parameter is a number, then
the indentation will be that many spaces.
Example:
text = JSON.stringify(['e', {pluribus: 'unum'}]);
// text is '["e",{"pluribus":"unum"}]'
text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
// text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
text = JSON.stringify([new Date()], function (key, value) {
return this[key] instanceof Date ?
'Date(' + this[key] + ')' : value;
});
// text is '["Date(---current time---)"]'
JSON.parse(text, reviver)
This method parses a JSON text to produce an object or array.
It can throw a SyntaxError exception.
The optional reviver parameter is a function that can filter and
transform the results. It receives each of the keys and values,
and its return value is used instead of the original value.
If it returns what it received, then the structure is not modified.
If it returns undefined then the member is deleted.
Example:
// Parse the text. Values that look like ISO date strings will
// be converted to Date objects.
myData = JSON.parse(text, function (key, value) {
var a;
if (typeof value === 'string') {
a =
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
if (a) {
return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
+a[5], +a[6]));
}
}
return value;
});
myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
var d;
if (typeof value === 'string' &&
value.slice(0, 5) === 'Date(' &&
value.slice(-1) === ')') {
d = new Date(value.slice(5, -1));
if (d) {
return d;
}
}
return value;
});
This is a reference implementation. You are free to copy, modify, or
redistribute.
*/
/*jslint evil: true, strict: false, regexp: false */
/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
lastIndex, length, parse, prototype, push, replace, slice, stringify,
test, toJSON, toString, valueOf
*/
// Create a JSON object only if one does not already exist. We create the
// methods in a closure to avoid creating global variables.
var JSON;
if (!JSON) {
JSON = {};
}
(function () {
"use strict";
function f(n) {
// Format integers to have at least two digits.
return n < 10 ? '0' + n : n;
}
if (typeof Date.prototype.toJSON !== 'function') {
Date.prototype.toJSON = function (key) {
return isFinite(this.valueOf()) ?
this.getUTCFullYear() + '-' +
f(this.getUTCMonth() + 1) + '-' +
f(this.getUTCDate()) + 'T' +
f(this.getUTCHours()) + ':' +
f(this.getUTCMinutes()) + ':' +
f(this.getUTCSeconds()) + 'Z' : null;
};
String.prototype.toJSON =
Number.prototype.toJSON =
Boolean.prototype.toJSON = function (key) {
return this.valueOf();
};
}
var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
gap,
indent,
meta = { // table of character substitutions
'\b': '\\b',
'\t': '\\t',
'\n': '\\n',
'\f': '\\f',
'\r': '\\r',
'"' : '\\"',
'\\': '\\\\'
},
rep;
function quote(string) {
// If the string contains no control characters, no quote characters, and no
// backslash characters, then we can safely slap some quotes around it.
// Otherwise we must also replace the offending characters with safe escape
// sequences.
escapable.lastIndex = 0;
return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
var c = meta[a];
return typeof c === 'string' ? c :
'\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
}) + '"' : '"' + string + '"';
}
function str(key, holder) {
// Produce a string from holder[key].
var i, // The loop counter.
k, // The member key.
v, // The member value.
length,
mind = gap,
partial,
value = holder[key];
// If the value has a toJSON method, call it to obtain a replacement value.
if (value && typeof value === 'object' &&
typeof value.toJSON === 'function') {
value = value.toJSON(key);
}
// If we were called with a replacer function, then call the replacer to
// obtain a replacement value.
if (typeof rep === 'function') {
value = rep.call(holder, key, value);
}
// What happens next depends on the value's type.
switch (typeof value) {
case 'string':
return quote(value);
case 'number':
// JSON numbers must be finite. Encode non-finite numbers as null.
return isFinite(value) ? String(value) : 'null';
case 'boolean':
case 'null':
// If the value is a boolean or null, convert it to a string. Note:
// typeof null does not produce 'null'. The case is included here in
// the remote chance that this gets fixed someday.
return String(value);
// If the type is 'object', we might be dealing with an object or an array or
// null.
case 'object':
// Due to a specification blunder in ECMAScript, typeof null is 'object',
// so watch out for that case.
if (!value) {
return 'null';
}
// Make an array to hold the partial results of stringifying this object value.
gap += indent;
partial = [];
// Is the value an array?
if (Object.prototype.toString.apply(value) === '[object Array]') {
// The value is an array. Stringify every element. Use null as a placeholder
// for non-JSON values.
length = value.length;
for (i = 0; i < length; i += 1) {
partial[i] = str(i, value) || 'null';
}
// Join all of the elements together, separated with commas, and wrap them in
// brackets.
v = partial.length === 0 ? '[]' : gap ?
'[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' :
'[' + partial.join(',') + ']';
gap = mind;
return v;
}
// If the replacer is an array, use it to select the members to be stringified.
if (rep && typeof rep === 'object') {
length = rep.length;
for (i = 0; i < length; i += 1) {
if (typeof rep[i] === 'string') {
k = rep[i];
v = str(k, value);
if (v) {
partial.push(quote(k) + (gap ? ': ' : ':') + v);
}
}
}
} else {
// Otherwise, iterate through all of the keys in the object.
for (k in value) {
if (Object.prototype.hasOwnProperty.call(value, k)) {
v = str(k, value);
if (v) {
partial.push(quote(k) + (gap ? ': ' : ':') + v);
}
}
}
}
// Join all of the member texts together, separated with commas,
// and wrap them in braces.
v = partial.length === 0 ? '{}' : gap ?
'{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' :
'{' + partial.join(',') + '}';
gap = mind;
return v;
}
}
// If the JSON object does not yet have a stringify method, give it one.
if (typeof JSON.stringify !== 'function') {
JSON.stringify = function (value, replacer, space) {
// The stringify method takes a value and an optional replacer, and an optional
// space parameter, and returns a JSON text. The replacer can be a function
// that can replace values, or an array of strings that will select the keys.
// A default replacer method can be provided. Use of the space parameter can
// produce text that is more easily readable.
var i;
gap = '';
indent = '';
// If the space parameter is a number, make an indent string containing that
// many spaces.
if (typeof space === 'number') {
for (i = 0; i < space; i += 1) {
indent += ' ';
}
// If the space parameter is a string, it will be used as the indent string.
} else if (typeof space === 'string') {
indent = space;
}
// If there is a replacer, it must be a function or an array.
// Otherwise, throw an error.
rep = replacer;
if (replacer && typeof replacer !== 'function' &&
(typeof replacer !== 'object' ||
typeof replacer.length !== 'number')) {
throw new Error('JSON.stringify');
}
// Make a fake root object containing our value under the key of ''.
// Return the result of stringifying the value.
return str('', {'': value});
};
}
// If the JSON object does not yet have a parse method, give it one.
if (typeof JSON.parse !== 'function') {
JSON.parse = function (text, reviver) {
// The parse method takes a text and an optional reviver function, and returns
// a JavaScript value if the text is a valid JSON text.
var j;
function walk(holder, key) {
// The walk method is used to recursively walk the resulting structure so
// that modifications can be made.
var k, v, value = holder[key];
if (value && typeof value === 'object') {
for (k in value) {
if (Object.prototype.hasOwnProperty.call(value, k)) {
v = walk(value, k);
if (v !== undefined) {
value[k] = v;
} else {
delete value[k];
}
}
}
}
return reviver.call(holder, key, value);
}
// Parsing happens in four stages. In the first stage, we replace certain
// Unicode characters with escape sequences. JavaScript handles many characters
// incorrectly, either silently deleting them, or treating them as line endings.
text = String(text);
cx.lastIndex = 0;
if (cx.test(text)) {
text = text.replace(cx, function (a) {
return '\\u' +
('0000' + a.charCodeAt(0).toString(16)).slice(-4);
});
}
// In the second stage, we run the text against regular expressions that look
// for non-JSON patterns. We are especially concerned with '()' and 'new'
// because they can cause invocation, and '=' because it can cause mutation.
// But just to be safe, we want to reject all unexpected forms.
// We split the second stage into 4 regexp operations in order to work around
// crippling inefficiencies in IE's and Safari's regexp engines. First we
// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
// replace all simple value tokens with ']' characters. Third, we delete all
// open brackets that follow a colon or comma or that begin the text. Finally,
// we look to see that the remaining characters are only whitespace or ']' or
// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
if (/^[\],:{}\s]*$/
.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
.replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
// In the third stage we use the eval function to compile the text into a
// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
// in JavaScript: it can begin a block or an object literal. We wrap the text
// in parens to eliminate the ambiguity.
j = eval('(' + text + ')');
// In the optional fourth stage, we recursively walk the new structure, passing
// each name/value pair to a reviver function for possible transformation.
return typeof reviver === 'function' ?
walk({'': j}, '') : j;
}
// If the text is not JSON parseable, then a SyntaxError is thrown.
throw new SyntaxError('JSON.parse');
};
}
}());

498
static/js/json2.js.old Normal file
View file

@ -0,0 +1,498 @@
/**
* 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.
*/
/*
http://www.JSON.org/json2.js
2008-09-01
Public Domain.
NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
See http://www.JSON.org/js.html
This file creates a global JSON object containing two methods: stringify
and parse.
JSON.stringify(value, replacer, space)
value any JavaScript value, usually an object or array.
replacer an optional parameter that determines how object
values are stringified for objects. It can be a
function or an array.
space an optional parameter that specifies the indentation
of nested structures. If it is omitted, the text will
be packed without extra whitespace. If it is a number,
it will specify the number of spaces to indent at each
level. If it is a string (such as '\t' or '&nbsp;'),
it contains the characters used to indent at each level.
This method produces a JSON text from a JavaScript value.
When an object value is found, if the object contains a toJSON
method, its toJSON method will be called and the result will be
stringified. A toJSON method does not serialize: it returns the
value represented by the name/value pair that should be serialized,
or undefined if nothing should be serialized. The toJSON method
will be passed the key associated with the value, and this will be
bound to the object holding the key.
For example, this would serialize Dates as ISO strings.
Date.prototype.toJSON = function (key) {
function f(n) {
// Format integers to have at least two digits.
return n < 10 ? '0' + n : n;
}
return this.getUTCFullYear() + '-' +
f(this.getUTCMonth() + 1) + '-' +
f(this.getUTCDate()) + 'T' +
f(this.getUTCHours()) + ':' +
f(this.getUTCMinutes()) + ':' +
f(this.getUTCSeconds()) + 'Z';
};
You can provide an optional replacer method. It will be passed the
key and value of each member, with this bound to the containing
object. The value that is returned from your method will be
serialized. If your method returns undefined, then the member will
be excluded from the serialization.
If the replacer parameter is an array, then it will be used to
select the members to be serialized. It filters the results such
that only members with keys listed in the replacer array are
stringified.
Values that do not have JSON representations, such as undefined or
functions, will not be serialized. Such values in objects will be
dropped; in arrays they will be replaced with null. You can use
a replacer function to replace those with JSON values.
JSON.stringify(undefined) returns undefined.
The optional space parameter produces a stringification of the
value that is filled with line breaks and indentation to make it
easier to read.
If the space parameter is a non-empty string, then that string will
be used for indentation. If the space parameter is a number, then
the indentation will be that many spaces.
Example:
text = JSON.stringify(['e', {pluribus: 'unum'}]);
// text is '["e",{"pluribus":"unum"}]'
text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
// text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
text = JSON.stringify([new Date()], function (key, value) {
return this[key] instanceof Date ?
'Date(' + this[key] + ')' : value;
});
// text is '["Date(---current time---)"]'
JSON.parse(text, reviver)
This method parses a JSON text to produce an object or array.
It can throw a SyntaxError exception.
The optional reviver parameter is a function that can filter and
transform the results. It receives each of the keys and values,
and its return value is used instead of the original value.
If it returns what it received, then the structure is not modified.
If it returns undefined then the member is deleted.
Example:
// Parse the text. Values that look like ISO date strings will
// be converted to Date objects.
myData = JSON.parse(text, function (key, value) {
var a;
if (typeof value === 'string') {
a =
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
if (a) {
return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
+a[5], +a[6]));
}
}
return value;
});
myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
var d;
if (typeof value === 'string' &&
value.slice(0, 5) === 'Date(' &&
value.slice(-1) === ')') {
d = new Date(value.slice(5, -1));
if (d) {
return d;
}
}
return value;
});
This is a reference implementation. You are free to copy, modify, or
redistribute.
This code should be minified before deployment.
See http://javascript.crockford.com/jsmin.html
USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
NOT CONTROL.
*/
/*jslint evil: true */
/*global JSON */
/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", call,
charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, getUTCMinutes,
getUTCMonth, getUTCSeconds, hasOwnProperty, join, lastIndex, length,
parse, propertyIsEnumerable, prototype, push, replace, slice, stringify,
test, toJSON, toString, valueOf
*/
// Create a JSON object only if one does not already exist. We create the
// methods in a closure to avoid creating global variables.
if (!this.JSON) {
JSON = {};
}
(function () {
function f(n) {
// Format integers to have at least two digits.
return n < 10 ? '0' + n : n;
}
if (typeof Date.prototype.toJSON !== 'function') {
Date.prototype.toJSON = function (key) {
return this.getUTCFullYear() + '-' +
f(this.getUTCMonth() + 1) + '-' +
f(this.getUTCDate()) + 'T' +
f(this.getUTCHours()) + ':' +
f(this.getUTCMinutes()) + ':' +
f(this.getUTCSeconds()) + 'Z';
};
String.prototype.toJSON =
Number.prototype.toJSON =
Boolean.prototype.toJSON = function (key) {
return this.valueOf();
};
}
// APPJET: escape all characters except non-control 7-bit ASCII (changed cx and escapeable)
var cx = /[\u0000-\u001f\u007f-\uffff]/g,
escapeable = /[\\\"\u0000-\u001f\u007f-\uffff]/g,
gap,
indent,
meta = { // table of character substitutions
'\b': '\\b',
'\t': '\\t',
'\n': '\\n',
'\f': '\\f',
'\r': '\\r',
'"' : '\\"',
'\\': '\\\\'
},
rep;
function quote(string) {
// If the string contains no control characters, no quote characters, and no
// backslash characters, then we can safely slap some quotes around it.
// Otherwise we must also replace the offending characters with safe escape
// sequences.
escapeable.lastIndex = 0;
return escapeable.test(string) ?
'"' + string.replace(escapeable, function (a) {
var c = meta[a];
if (typeof c === 'string') {
return c;
}
return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
}) + '"' :
'"' + string + '"';
}
function str(key, holder) {
// Produce a string from holder[key].
var i, // The loop counter.
k, // The member key.
v, // The member value.
length,
mind = gap,
partial,
value = holder[key];
// If the value has a toJSON method, call it to obtain a replacement value.
if (value && typeof value === 'object' &&
typeof value.toJSON === 'function') {
value = value.toJSON(key);
}
// If we were called with a replacer function, then call the replacer to
// obtain a replacement value.
if (typeof rep === 'function') {
value = rep.call(holder, key, value);
}
// What happens next depends on the value's type.
switch (typeof value) {
case 'string':
return quote(value);
case 'number':
// JSON numbers must be finite. Encode non-finite numbers as null.
return isFinite(value) ? String(value) : 'null';
case 'boolean':
case 'null':
// If the value is a boolean or null, convert it to a string. Note:
// typeof null does not produce 'null'. The case is included here in
// the remote chance that this gets fixed someday.
return String(value);
// If the type is 'object', we might be dealing with an object or an array or
// null.
case 'object':
// Due to a specification blunder in ECMAScript, typeof null is 'object',
// so watch out for that case.
if (!value) {
return 'null';
}
// Make an array to hold the partial results of stringifying this object value.
gap += indent;
partial = [];
// If the object has a dontEnum length property, we'll treat it as an array.
if (typeof value.length === 'number' &&
!value.propertyIsEnumerable('length')) {
// The object is an array. Stringify every element. Use null as a placeholder
// for non-JSON values.
length = value.length;
for (i = 0; i < length; i += 1) {
partial[i] = str(i, value) || 'null';
}
// Join all of the elements together, separated with commas, and wrap them in
// brackets.
v = partial.length === 0 ? '[]' :
gap ? '[\n' + gap +
partial.join(',\n' + gap) + '\n' +
mind + ']' :
'[' + partial.join(',') + ']';
gap = mind;
return v;
}
// If the replacer is an array, use it to select the members to be stringified.
if (rep && typeof rep === 'object') {
length = rep.length;
for (i = 0; i < length; i += 1) {
k = rep[i];
if (typeof k === 'string') {
v = str(k, value);
if (v) {
partial.push(quote(k) + (gap ? ': ' : ':') + v);
}
}
}
} else {
// Otherwise, iterate through all of the keys in the object.
for (k in value) {
if (Object.hasOwnProperty.call(value, k)) {
v = str(k, value);
if (v) {
partial.push(quote(k) + (gap ? ': ' : ':') + v);
}
}
}
}
// Join all of the member texts together, separated with commas,
// and wrap them in braces.
v = partial.length === 0 ? '{}' :
gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' +
mind + '}' : '{' + partial.join(',') + '}';
gap = mind;
return v;
}
}
// If the JSON object does not yet have a stringify method, give it one.
if (typeof JSON.stringify !== 'function') {
JSON.stringify = function (value, replacer, space) {
// The stringify method takes a value and an optional replacer, and an optional
// space parameter, and returns a JSON text. The replacer can be a function
// that can replace values, or an array of strings that will select the keys.
// A default replacer method can be provided. Use of the space parameter can
// produce text that is more easily readable.
var i;
gap = '';
indent = '';
// If the space parameter is a number, make an indent string containing that
// many spaces.
if (typeof space === 'number') {
for (i = 0; i < space; i += 1) {
indent += ' ';
}
// If the space parameter is a string, it will be used as the indent string.
} else if (typeof space === 'string') {
indent = space;
}
// If there is a replacer, it must be a function or an array.
// Otherwise, throw an error.
rep = replacer;
if (replacer && typeof replacer !== 'function' &&
(typeof replacer !== 'object' ||
typeof replacer.length !== 'number')) {
throw new Error('JSON.stringify');
}
// Make a fake root object containing our value under the key of ''.
// Return the result of stringifying the value.
return str('', {'': value});
};
}
// If the JSON object does not yet have a parse method, give it one.
if (typeof JSON.parse !== 'function') {
JSON.parse = function (text, reviver) {
// The parse method takes a text and an optional reviver function, and returns
// a JavaScript value if the text is a valid JSON text.
var j;
function walk(holder, key) {
// The walk method is used to recursively walk the resulting structure so
// that modifications can be made.
var k, v, value = holder[key];
if (value && typeof value === 'object') {
for (k in value) {
if (Object.hasOwnProperty.call(value, k)) {
v = walk(value, k);
if (v !== undefined) {
value[k] = v;
} else {
delete value[k];
}
}
}
}
return reviver.call(holder, key, value);
}
// Parsing happens in four stages. In the first stage, we replace certain
// Unicode characters with escape sequences. JavaScript handles many characters
// incorrectly, either silently deleting them, or treating them as line endings.
cx.lastIndex = 0;
if (cx.test(text)) {
text = text.replace(cx, function (a) {
return '\\u' +
('0000' + a.charCodeAt(0).toString(16)).slice(-4);
});
}
// In the second stage, we run the text against regular expressions that look
// for non-JSON patterns. We are especially concerned with '()' and 'new'
// because they can cause invocation, and '=' because it can cause mutation.
// But just to be safe, we want to reject all unexpected forms.
// We split the second stage into 4 regexp operations in order to work around
// crippling inefficiencies in IE's and Safari's regexp engines. First we
// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
// replace all simple value tokens with ']' characters. Third, we delete all
// open brackets that follow a colon or comma or that begin the text. Finally,
// we look to see that the remaining characters are only whitespace or ']' or
// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
if (/^[\],:{}\s]*$/.
test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').
replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
// In the third stage we use the eval function to compile the text into a
// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
// in JavaScript: it can begin a block or an object literal. We wrap the text
// in parens to eliminate the ambiguity.
j = window['ev'+'al']('(' + text + ')');
// In the optional fourth stage, we recursively walk the new structure, passing
// each name/value pair to a reviver function for possible transformation.
return typeof reviver === 'function' ?
walk({'': j}, '') : j;
}
// If the text is not JSON parseable, then a SyntaxError is thrown.
throw new SyntaxError('JSON.parse');
};
}
})();

View file

@ -0,0 +1,290 @@
// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.linestylefilter
// %APPJET%: import("etherpad.collab.ace.easysync2.Changeset");
// %APPJET%: import("etherpad.admin.plugins");
/**
* 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.
*/
// requires: easysync2.Changeset
// requires: top
// requires: plugins
// requires: undefined
var linestylefilter = {};
linestylefilter.ATTRIB_CLASSES = {
'bold':'tag:b',
'italic':'tag:i',
'underline':'tag:u',
'strikethrough':'tag:s'
};
linestylefilter.getAuthorClassName = function(author) {
return "author-"+author.replace(/[^a-y0-9]/g, function(c) {
if (c == ".") return "-";
return 'z'+c.charCodeAt(0)+'z';
});
};
// lineLength is without newline; aline includes newline,
// but may be falsy if lineLength == 0
linestylefilter.getLineStyleFilter = function(lineLength, aline,
textAndClassFunc, apool) {
var plugins_;
if (typeof(plugins)!='undefined') {
plugins_ = plugins;
} else {
plugins_ = parent.parent.plugins;
}
if (lineLength == 0) return textAndClassFunc;
var nextAfterAuthorColors = textAndClassFunc;
var authorColorFunc = (function() {
var lineEnd = lineLength;
var curIndex = 0;
var extraClasses;
var leftInAuthor;
function attribsToClasses(attribs) {
var classes = '';
Changeset.eachAttribNumber(attribs, function(n) {
var key = apool.getAttribKey(n);
if (key) {
var value = apool.getAttribValue(n);
if (value) {
if (key == 'author') {
classes += ' '+linestylefilter.getAuthorClassName(value);
}
else if (key == 'list') {
classes += ' list:'+value;
}
else if (linestylefilter.ATTRIB_CLASSES[key]) {
classes += ' '+linestylefilter.ATTRIB_CLASSES[key];
} else {
classes += plugins_.callHookStr("aceAttribsToClasses", {linestylefilter:linestylefilter, key:key, value:value}, " ", " ", "");
}
}
}
});
return classes.substring(1);
}
var attributionIter = Changeset.opIterator(aline);
var nextOp, nextOpClasses;
function goNextOp() {
nextOp = attributionIter.next();
nextOpClasses = (nextOp.opcode && attribsToClasses(nextOp.attribs));
}
goNextOp();
function nextClasses() {
if (curIndex < lineEnd) {
extraClasses = nextOpClasses;
leftInAuthor = nextOp.chars;
goNextOp();
while (nextOp.opcode && nextOpClasses == extraClasses) {
leftInAuthor += nextOp.chars;
goNextOp();
}
}
}
nextClasses();
return function(txt, cls) {
while (txt.length > 0) {
if (leftInAuthor <= 0) {
// prevent infinite loop if something funny's going on
return nextAfterAuthorColors(txt, cls);
}
var spanSize = txt.length;
if (spanSize > leftInAuthor) {
spanSize = leftInAuthor;
}
var curTxt = txt.substring(0, spanSize);
txt = txt.substring(spanSize);
nextAfterAuthorColors(curTxt, (cls&&cls+" ")+extraClasses);
curIndex += spanSize;
leftInAuthor -= spanSize;
if (leftInAuthor == 0) {
nextClasses();
}
}
};
})();
return authorColorFunc;
};
linestylefilter.getAtSignSplitterFilter = function(lineText,
textAndClassFunc) {
var at = /@/g;
at.lastIndex = 0;
var splitPoints = null;
var execResult;
while ((execResult = at.exec(lineText))) {
if (! splitPoints) {
splitPoints = [];
}
splitPoints.push(execResult.index);
}
if (! splitPoints) return textAndClassFunc;
return linestylefilter.textAndClassFuncSplitter(textAndClassFunc,
splitPoints);
};
linestylefilter.getRegexpFilter = function (regExp, tag) {
return function (lineText, textAndClassFunc) {
regExp.lastIndex = 0;
var regExpMatchs = null;
var splitPoints = null;
var execResult;
while ((execResult = regExp.exec(lineText))) {
if (! regExpMatchs) {
regExpMatchs = [];
splitPoints = [];
}
var startIndex = execResult.index;
var regExpMatch = execResult[0];
regExpMatchs.push([startIndex, regExpMatch]);
splitPoints.push(startIndex, startIndex + regExpMatch.length);
}
if (! regExpMatchs) return textAndClassFunc;
function regExpMatchForIndex(idx) {
for(var k=0; k<regExpMatchs.length; k++) {
var u = regExpMatchs[k];
if (idx >= u[0] && idx < u[0]+u[1].length) {
return u[1];
}
}
return false;
}
var handleRegExpMatchsAfterSplit = (function() {
var curIndex = 0;
return function(txt, cls) {
var txtlen = txt.length;
var newCls = cls;
var regExpMatch = regExpMatchForIndex(curIndex);
if (regExpMatch) {
newCls += " "+tag+":"+regExpMatch;
}
textAndClassFunc(txt, newCls);
curIndex += txtlen;
};
})();
return linestylefilter.textAndClassFuncSplitter(handleRegExpMatchsAfterSplit,
splitPoints);
};
};
linestylefilter.REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/;
linestylefilter.REGEX_URLCHAR = new RegExp('('+/[-:@a-zA-Z0-9_.,~%+\/\\?=&#;()$]/.source+'|'+linestylefilter.REGEX_WORDCHAR.source+')');
linestylefilter.REGEX_URL = new RegExp(/(?:(?:https?|s?ftp|ftps|file|smb|afp|nfs|(x-)?man|gopher|txmt):\/\/|mailto:)/.source+linestylefilter.REGEX_URLCHAR.source+'*(?![:.,;])'+linestylefilter.REGEX_URLCHAR.source, 'g');
linestylefilter.getURLFilter = linestylefilter.getRegexpFilter(
linestylefilter.REGEX_URL, 'url');
linestylefilter.textAndClassFuncSplitter = function(func, splitPointsOpt) {
var nextPointIndex = 0;
var idx = 0;
// don't split at 0
while (splitPointsOpt &&
nextPointIndex < splitPointsOpt.length &&
splitPointsOpt[nextPointIndex] == 0) {
nextPointIndex++;
}
function spanHandler(txt, cls) {
if ((! splitPointsOpt) || nextPointIndex >= splitPointsOpt.length) {
func(txt, cls);
idx += txt.length;
}
else {
var splitPoints = splitPointsOpt;
var pointLocInSpan = splitPoints[nextPointIndex] - idx;
var txtlen = txt.length;
if (pointLocInSpan >= txtlen) {
func(txt, cls);
idx += txt.length;
if (pointLocInSpan == txtlen) {
nextPointIndex++;
}
}
else {
if (pointLocInSpan > 0) {
func(txt.substring(0, pointLocInSpan), cls);
idx += pointLocInSpan;
}
nextPointIndex++;
// recurse
spanHandler(txt.substring(pointLocInSpan), cls);
}
}
}
return spanHandler;
};
linestylefilter.getFilterStack = function(lineText, textAndClassFunc, browser) {
var func = linestylefilter.getURLFilter(lineText, textAndClassFunc);
var plugins_;
if (typeof(plugins)!='undefined') {
plugins_ = plugins;
} else {
plugins_ = parent.parent.plugins;
}
var hookFilters = plugins_.callHook(
"aceGetFilterStack", {linestylefilter:linestylefilter, browser:browser});
hookFilters.map(function (hookFilter) {
func = hookFilter(lineText, func);
});
if (browser !== undefined && browser.msie) {
// IE7+ will take an e-mail address like <foo@bar.com> and linkify it to foo@bar.com.
// We then normalize it back to text with no angle brackets. It's weird. So always
// break spans at an "at" sign.
func = linestylefilter.getAtSignSplitterFilter(
lineText, func);
}
return func;
};
// domLineObj is like that returned by domline.createDomLine
linestylefilter.populateDomLine = function(textLine, aline, apool,
domLineObj) {
// remove final newline from text if any
var text = textLine;
if (text.slice(-1) == '\n') {
text = text.substring(0, text.length-1);
}
function textAndClassFunc(tokenText, tokenClass) {
domLineObj.appendSpan(tokenText, tokenClass);
}
var func = linestylefilter.getFilterStack(text, textAndClassFunc);
func = linestylefilter.getLineStyleFilter(text.length, aline,
func, apool);
func(text, '');
};

567
static/js/pad2.js Normal file
View file

@ -0,0 +1,567 @@
/**
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka
*
* 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.
*/
/* global $, window */
var socket;
$(document).ready(function() {
handshake();
});
$(window).unload(function() {
pad.dispose();
});
function createCookie(name,value,days) {
if (days) {
var date = new Date();
date.setTime(date.getTime()+(days*24*60*60*1000));
var expires = "; expires="+date.toGMTString();
}
else var expires = "";
document.cookie = name+"="+value+expires+"; path=/";
}
function readCookie(name) {
var nameEQ = name + "=";
var ca = document.cookie.split(';');
for(var i=0;i < ca.length;i++) {
var c = ca[i];
while (c.charAt(0)==' ') c = c.substring(1,c.length);
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
}
return null;
}
function randomString() {
var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz";
var string_length = 20;
var randomstring = '';
for (var i=0; i<string_length; i++) {
var rnum = Math.floor(Math.random() * chars.length);
randomstring += chars.substring(rnum,rnum+1);
}
return "t." + randomstring;
}
function handshake()
{
socket = new io.Socket();
socket.connect();
socket.on('connect', function(){
var padId= document.URL.substring(document.URL.lastIndexOf("/")+1);
var token = readCookie("token");
if(token == null)
{
token = randomString();
createCookie("token", token, 60);
}
var msg = { "type":"CLIENT_READY",
"padId": padId,
"token": token,
"protocolVersion": 1};
socket.send(msg);
});
var receivedClientVars=false;
var initalized = false;
socket.on('message', function(obj){
if(!receivedClientVars)
{
receivedClientVars=true;
clientVars = obj;
clientVars.userAgent=navigator.userAgent;
clientVars.collab_client_vars.clientAgent=navigator.userAgent;
pad.init();
initalized=true;
}
else
{
if(!initalized)
{
setTimeOut(this(obj));
}
}
});
}
var pad = {
// don't access these directly from outside this file, except
// for debugging
collabClient: null,
myUserInfo: null,
diagnosticInfo: {},
initTime: 0,
clientTimeOffset: (+new Date()) - clientVars.serverTimestamp,
preloadedImages: false,
padOptions: {},
// these don't require init; clientVars should all go through here
getPadId: function() { return clientVars.padId; },
getClientIp: function() { return clientVars.clientIp; },
getIsProPad: function() { return clientVars.isProPad; },
getColorPalette: function() { return clientVars.colorPalette; },
getDisplayUserAgent: function() {
return padutils.uaDisplay(clientVars.userAgent);
},
getIsDebugEnabled: function() { return clientVars.debugEnabled; },
getPrivilege: function(name) { return clientVars.accountPrivs[name]; },
getUserIsGuest: function() { return clientVars.userIsGuest; },
//
getUserId: function() { return pad.myUserInfo.userId; },
getUserName: function() { return pad.myUserInfo.name; },
sendClientMessage: function(msg) {
pad.collabClient.sendClientMessage(msg);
},
init: function() {
pad.diagnosticInfo.uniqueId = padutils.uniqueId();
pad.initTime = +(new Date());
pad.padOptions = clientVars.initialOptions;
if ((! $.browser.msie) &&
(! ($.browser.mozilla && $.browser.version.indexOf("1.8.") == 0))) {
document.domain = document.domain; // for comet
}
// for IE
if ($.browser.msie) {
try {
doc.execCommand("BackgroundImageCache", false, true);
} catch (e) {}
}
// order of inits is important here:
padcookie.init(clientVars.cookiePrefsToSet);
$("#widthprefcheck").click(pad.toggleWidthPref);
$("#sidebarcheck").click(pad.toggleSidebar);
pad.myUserInfo = {
userId: clientVars.userId,
name: clientVars.userName,
ip: pad.getClientIp(),
colorId: clientVars.userColor,
userAgent: pad.getDisplayUserAgent()
};
if (clientVars.specialKey) {
pad.myUserInfo.specialKey = clientVars.specialKey;
if (clientVars.specialKeyTranslation) {
$("#specialkeyarea").html("mode: "+
String(clientVars.specialKeyTranslation).toUpperCase());
}
}
paddocbar.init({isTitleEditable: pad.getIsProPad(),
initialTitle:clientVars.initialTitle,
initialPassword:clientVars.initialPassword,
guestPolicy: pad.padOptions.guestPolicy
});
padimpexp.init();
padsavedrevs.init(clientVars.initialRevisionList);
padeditor.init(postAceInit, pad.padOptions.view || {});
paduserlist.init(pad.myUserInfo);
padchat.init(clientVars.chatHistory, pad.myUserInfo);
padconnectionstatus.init();
padmodals.init();
pad.collabClient =
getCollabClient(padeditor.ace,
clientVars.collab_client_vars,
pad.myUserInfo,
{ colorPalette: pad.getColorPalette() });
pad.collabClient.setOnUserJoin(pad.handleUserJoin);
pad.collabClient.setOnUpdateUserInfo(pad.handleUserUpdate);
pad.collabClient.setOnUserLeave(pad.handleUserLeave);
pad.collabClient.setOnClientMessage(pad.handleClientMessage);
pad.collabClient.setOnServerMessage(pad.handleServerMessage);
pad.collabClient.setOnChannelStateChange(pad.handleChannelStateChange);
pad.collabClient.setOnInternalAction(pad.handleCollabAction);
function postAceInit() {
padeditbar.init();
setTimeout(function() { padeditor.ace.focus(); }, 0);
}
},
dispose: function() {
padeditor.dispose();
},
notifyChangeName: function(newName) {
pad.myUserInfo.name = newName;
pad.collabClient.updateUserInfo(pad.myUserInfo);
padchat.handleUserJoinOrUpdate(pad.myUserInfo);
},
notifyChangeColor: function(newColorId) {
pad.myUserInfo.colorId = newColorId;
pad.collabClient.updateUserInfo(pad.myUserInfo);
padchat.handleUserJoinOrUpdate(pad.myUserInfo);
},
notifyChangeTitle: function(newTitle) {
pad.collabClient.sendClientMessage({
type: 'padtitle',
title: newTitle,
changedBy: pad.myUserInfo.name || "unnamed"
});
},
notifyChangePassword: function(newPass) {
pad.collabClient.sendClientMessage({
type: 'padpassword',
password: newPass,
changedBy: pad.myUserInfo.name || "unnamed"
});
},
changePadOption: function(key, value) {
var options = {};
options[key] = value;
pad.handleOptionsChange(options);
pad.collabClient.sendClientMessage({
type: 'padoptions',
options: options,
changedBy: pad.myUserInfo.name || "unnamed"
});
},
changeViewOption: function(key, value) {
var options = {view: {}};
options.view[key] = value;
pad.handleOptionsChange(options);
pad.collabClient.sendClientMessage({
type: 'padoptions',
options: options,
changedBy: pad.myUserInfo.name || "unnamed"
});
},
handleOptionsChange: function(opts) {
// opts object is a full set of options or just
// some options to change
if (opts.view) {
if (! pad.padOptions.view) {
pad.padOptions.view = {};
}
for(var k in opts.view) {
pad.padOptions.view[k] = opts.view[k];
}
padeditor.setViewOptions(pad.padOptions.view);
}
if (opts.guestPolicy) {
// order important here
pad.padOptions.guestPolicy = opts.guestPolicy;
paddocbar.setGuestPolicy(opts.guestPolicy);
}
},
getPadOptions: function() {
// caller shouldn't mutate the object
return pad.padOptions;
},
isPadPublic: function() {
return (! pad.getIsProPad()) || (pad.getPadOptions().guestPolicy == 'allow');
},
suggestUserName: function(userId, name) {
pad.collabClient.sendClientMessage({
type: 'suggestUserName',
unnamedId: userId,
newName: name
});
},
handleUserJoin: function(userInfo) {
paduserlist.userJoinOrUpdate(userInfo);
padchat.handleUserJoinOrUpdate(userInfo);
},
handleUserUpdate: function(userInfo) {
paduserlist.userJoinOrUpdate(userInfo);
padchat.handleUserJoinOrUpdate(userInfo);
},
handleUserLeave: function(userInfo) {
paduserlist.userLeave(userInfo);
padchat.handleUserLeave(userInfo);
},
handleClientMessage: function(msg) {
if (msg.type == 'suggestUserName') {
if (msg.unnamedId == pad.myUserInfo.userId && msg.newName &&
! pad.myUserInfo.name) {
pad.notifyChangeName(msg.newName);
paduserlist.setMyUserInfo(pad.myUserInfo);
}
}
else if (msg.type == 'chat') {
padchat.receiveChat(msg);
}
else if (msg.type == 'padtitle') {
paddocbar.changeTitle(msg.title);
}
else if (msg.type == 'padpassword') {
paddocbar.changePassword(msg.password);
}
else if (msg.type == 'newRevisionList') {
padsavedrevs.newRevisionList(msg.revisionList);
}
else if (msg.type == 'revisionLabel') {
padsavedrevs.newRevisionList(msg.revisionList);
}
else if (msg.type == 'padoptions') {
var opts = msg.options;
pad.handleOptionsChange(opts);
}
else if (msg.type == 'guestanswer') {
// someone answered a prompt, remove it
paduserlist.removeGuestPrompt(msg.guestId);
}
},
editbarClick: function(cmd) {
if (padeditbar) {
padeditbar.toolbarClick(cmd);
}
},
dmesg: function(m) {
if (pad.getIsDebugEnabled()) {
var djs = $('#djs').get(0);
var wasAtBottom = (djs.scrollTop - (djs.scrollHeight - $(djs).height())
>= -20);
$('#djs').append('<p>'+m+'</p>');
if (wasAtBottom) {
djs.scrollTop = djs.scrollHeight;
}
}
},
handleServerMessage: function(m) {
if (m.type == 'NOTICE') {
if (m.text) {
alertBar.displayMessage(function (abar) {
abar.find("#servermsgdate").html(" ("+padutils.simpleDateTime(new Date)+")");
abar.find("#servermsgtext").html(m.text);
});
}
if (m.js) {
window['ev'+'al'](m.js);
}
}
else if (m.type == 'GUEST_PROMPT') {
paduserlist.showGuestPrompt(m.userId, m.displayName);
}
},
handleChannelStateChange: function(newState, message) {
var oldFullyConnected = !! padconnectionstatus.isFullyConnected();
var wasConnecting = (padconnectionstatus.getStatus().what == 'connecting');
if (newState == "CONNECTED") {
padconnectionstatus.connected();
}
else if (newState == "RECONNECTING") {
padconnectionstatus.reconnecting();
}
else if (newState == "DISCONNECTED") {
pad.diagnosticInfo.disconnectedMessage = message;
pad.diagnosticInfo.padInitTime = pad.initTime;
pad.asyncSendDiagnosticInfo();
if (typeof window.ajlog == "string") { window.ajlog += ("Disconnected: "+message+'\n'); }
padeditor.disable();
padeditbar.disable();
paddocbar.disable();
padimpexp.disable();
padconnectionstatus.disconnected(message);
}
var newFullyConnected = !! padconnectionstatus.isFullyConnected();
if (newFullyConnected != oldFullyConnected) {
pad.handleIsFullyConnected(newFullyConnected, wasConnecting);
}
},
handleIsFullyConnected: function(isConnected, isInitialConnect) {
// load all images referenced from CSS, one at a time,
// starting one second after connection is first established.
if (isConnected && ! pad.preloadedImages) {
window.setTimeout(function() {
if (! pad.preloadedImages) {
pad.preloadImages();
pad.preloadedImages = true;
}
}, 1000);
}
padsavedrevs.handleIsFullyConnected(isConnected);
pad.determineSidebarVisibility(isConnected && ! isInitialConnect);
},
determineSidebarVisibility: function(asNowConnectedFeedback) {
if (pad.isFullyConnected()) {
var setSidebarVisibility =
padutils.getCancellableAction(
"set-sidebar-visibility",
function() {
$("body").toggleClass('hidesidebar',
!! padcookie.getPref('hideSidebar'));
});
window.setTimeout(setSidebarVisibility,
asNowConnectedFeedback ? 3000 : 0);
}
else {
padutils.cancelActions("set-sidebar-visibility");
$("body").removeClass('hidesidebar');
}
},
handleCollabAction: function(action) {
if (action == "commitPerformed") {
padeditbar.setSyncStatus("syncing");
}
else if (action == "newlyIdle") {
padeditbar.setSyncStatus("done");
}
},
hideServerMessage: function() {
alertBar.hideMessage();
},
asyncSendDiagnosticInfo: function() {
pad.diagnosticInfo.collabDiagnosticInfo = pad.collabClient.getDiagnosticInfo();
window.setTimeout(function() {
$.ajax({
type: 'post',
url: '/ep/pad/connection-diagnostic-info',
data: {padId: pad.getPadId(), diagnosticInfo: JSON.stringify(pad.diagnosticInfo)},
success: function() {},
error: function() {}
});
}, 0);
},
forceReconnect: function() {
$('form#reconnectform input.padId').val(pad.getPadId());
pad.diagnosticInfo.collabDiagnosticInfo = pad.collabClient.getDiagnosticInfo();
$('form#reconnectform input.diagnosticInfo').val(JSON.stringify(pad.diagnosticInfo));
$('form#reconnectform input.missedChanges').val(JSON.stringify(pad.collabClient.getMissedChanges()));
$('form#reconnectform').submit();
},
toggleWidthPref: function() {
var newValue = ! padcookie.getPref('fullWidth');
padcookie.setPref('fullWidth', newValue);
$("#widthprefcheck").toggleClass('widthprefchecked', !!newValue).toggleClass(
'widthprefunchecked', !newValue);
pad.handleWidthChange();
},
toggleSidebar: function() {
var newValue = ! padcookie.getPref('hideSidebar');
padcookie.setPref('hideSidebar', newValue);
$("#sidebarcheck").toggleClass('sidebarchecked', !newValue).toggleClass(
'sidebarunchecked', !!newValue);
pad.determineSidebarVisibility();
},
handleWidthChange: function() {
var isFullWidth = padcookie.getPref('fullWidth');
if (isFullWidth) {
$("body").addClass('fullwidth').removeClass('limwidth').removeClass(
'squish1width').removeClass('squish2width');
}
else {
$("body").addClass('limwidth').removeClass('fullwidth');
var pageWidth = $(window).width();
$("body").toggleClass('squish1width', (pageWidth < 912 && pageWidth > 812)).toggleClass(
'squish2width', (pageWidth <= 812));
}
},
// this is called from code put into a frame from the server:
handleImportExportFrameCall: function(callName, varargs) {
padimpexp.handleFrameCall.call(padimpexp, callName,
Array.prototype.slice.call(arguments, 1));
},
callWhenNotCommitting: function(f) {
pad.collabClient.callWhenNotCommitting(f);
},
getCollabRevisionNumber: function() {
return pad.collabClient.getCurrentRevisionNumber();
},
isFullyConnected: function() {
return padconnectionstatus.isFullyConnected();
},
addHistoricalAuthors: function(data) {
if (! pad.collabClient) {
window.setTimeout(function() { pad.addHistoricalAuthors(data); },
1000);
}
else {
pad.collabClient.addHistoricalAuthors(data);
}
},
preloadImages: function() {
var images = [
'/static/img/jun09/pad/feedbackbox2.gif',
'/static/img/jun09/pad/sharebox4.gif',
'/static/img/jun09/pad/sharedistri.gif',
'/static/img/jun09/pad/colorpicker.gif',
'/static/img/jun09/pad/docbarstates.png',
'/static/img/jun09/pad/overlay.png'
];
function loadNextImage() {
if (images.length == 0) {
return;
}
var img = new Image();
img.src = images.shift();
if (img.complete) {
scheduleLoadNextImage();
}
else {
$(img).bind('error load onreadystatechange', scheduleLoadNextImage);
}
}
function scheduleLoadNextImage() {
window.setTimeout(loadNextImage, 0);
}
scheduleLoadNextImage();
}
};
var alertBar = (function() {
var animator = padutils.makeShowHideAnimator(arriveAtAnimationState, false, 25, 400);
function arriveAtAnimationState(state) {
if (state == -1) {
$("#alertbar").css('opacity', 0).css('display', 'block');
}
else if (state == 0) {
$("#alertbar").css('opacity', 1);
}
else if (state == 1) {
$("#alertbar").css('opacity', 0).css('display', 'none');
}
else if (state < 0) {
$("#alertbar").css('opacity', state+1);
}
else if (state > 0) {
$("#alertbar").css('opacity', 1 - state);
}
}
var self = {
displayMessage: function(setupFunc) {
animator.show();
setupFunc($("#alertbar"));
},
hideMessage: function() {
animator.hide();
}
};
return self;
}());

295
static/js/pad_chat.js Normal file
View file

@ -0,0 +1,295 @@
/**
* 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 padchat = (function(){
var numToAuthorMap = [''];
var authorColorArray = [null];
var authorToNumMap = {};
var chatLinesByDay = []; // {day:'2009-06-17', lines: [...]}
var oldestHistoricalLine = 0;
var loadingMoreHistory = false;
var HISTORY_LINES_TO_LOAD_AT_A_TIME = 50;
function authorToNum(author, dontAddIfAbsent) {
if ((typeof authorToNumMap[author]) == "number") {
return authorToNumMap[author];
}
else if (dontAddIfAbsent) {
return -1;
}
else {
var n = numToAuthorMap.length;
numToAuthorMap.push(author);
authorToNumMap[author] = n;
return n;
}
}
function getDateNumCSSDayString(dateNum) {
var d = new Date(+dateNum);
var year = String(d.getFullYear());
var month = ("0"+String(d.getMonth()+1)).slice(-2);
var day = ("0"+String(d.getDate())).slice(-2);
return year+"-"+month+"-"+day;
}
function getDateNumHumanDayString(dateNum) {
var d = new Date(+dateNum);
var monthName = (["January", "February", "March",
"April", "May", "June", "July", "August", "September",
"October", "November", "December"])[d.getMonth()];
var dayOfMonth = d.getDate();
var year = d.getFullYear();
return monthName+" "+dayOfMonth+", "+year;
}
function ensureChatDay(time) {
var day = getDateNumCSSDayString(time);
var dayIndex = padutils.binarySearch(chatLinesByDay.length, function(n) {
return chatLinesByDay[n].day >= day;
});
if (dayIndex >= chatLinesByDay.length ||
chatLinesByDay[dayIndex].day != day) {
// add new day to chat display!
chatLinesByDay.splice(dayIndex, 0, {day: day, lines: []});
var dayHtml = '<div class="chatday" id="chatday'+day+'">'+
'<h2 class="dayheader">'+getDateNumHumanDayString(time)+
'</h2></div>';
var dayDivs = $("#chatlines .chatday");
if (dayIndex == dayDivs.length) {
$("#chatlines").append(dayHtml);
}
else {
dayDivs.eq(dayIndex).before(dayHtml);
}
}
return dayIndex;
}
function addChatLine(userId, time, name, lineText, addBefore) {
var dayIndex = ensureChatDay(time);
var dayDiv = $("#chatday"+getDateNumCSSDayString(time));
var d = new Date(+time);
var hourmin = d.getHours()+":"+("0"+d.getMinutes()).slice(-2);
var nameHtml;
if (name) {
nameHtml = padutils.escapeHtml(name);
}
else {
nameHtml = "<i>unnamed</i>";
}
var chatlineClass = "chatline";
if (userId) {
var authorNum = authorToNum(userId);
chatlineClass += " chatauthor"+authorNum;
}
var textHtml = padutils.escapeHtmlWithClickableLinks(lineText, '_blank');
var lineNode = $('<div class="'+chatlineClass+'">'+
'<span class="chatlinetime">'+hourmin+' </span>'+
'<span class="chatlinename">'+nameHtml+': </span>'+
'<span class="chatlinetext">'+textHtml+'</span></div>');
var linesArray = chatLinesByDay[dayIndex].lines;
var lineObj = {userId:userId, time:time, name:name, lineText:lineText};
if (addBefore) {
dayDiv.find("h2").after(lineNode);
linesArray.splice(0, 0, lineObj);
}
else {
dayDiv.append(lineNode);
linesArray.push(lineObj);
}
if (userId) {
var color = getAuthorCSSColor(userId);
if (color) {
lineNode.css('background', color);
}
}
return {lineNode:lineNode};
}
function receiveChatHistoryBlock(block) {
for(var a in block.historicalAuthorData) {
var data = block.historicalAuthorData[a];
var n = authorToNum(a);
if (! authorColorArray[n]) {
// no data about this author, use historical info
authorColorArray[n] = { colorId: data.colorId, faded: true };
}
}
oldestHistoricalLine = block.start;
var lines = block.lines;
for(var i=lines.length-1; i>=0; i--) {
var line = lines[i];
addChatLine(line.userId, line.time, line.name, line.lineText, true);
}
if (oldestHistoricalLine > 0) {
$("a#chatloadmore").css('display', 'block');
}
else {
$("a#chatloadmore").css('display', 'none');
}
}
function fadeColor(colorCSS) {
var color = colorutils.css2triple(colorCSS);
color = colorutils.blend(color, [1,1,1], 0.5);
return colorutils.triple2css(color);
}
function getAuthorCSSColor(author) {
var n = authorToNum(author, true);
if (n < 0) {
return '';
}
else {
var cdata = authorColorArray[n];
if (! cdata) {
return '';
}
else {
var c = pad.getColorPalette()[cdata.colorId];
if (cdata.faded) {
c = fadeColor(c);
}
return c;
}
}
}
function changeAuthorColorData(author, cdata) {
var n = authorToNum(author);
authorColorArray[n] = cdata;
var cssColor = getAuthorCSSColor(author);
if (cssColor) {
$("#chatlines .chatauthor"+n).css('background',cssColor);
}
}
function sendChat() {
var lineText = $("#chatentrybox").val();
if (lineText) {
$("#chatentrybox").val('').focus();
var msg = {
type: 'chat',
userId: pad.getUserId(),
lineText: lineText,
senderName: pad.getUserName(),
authId: pad.getUserId()
};
pad.sendClientMessage(msg);
self.receiveChat(msg);
self.scrollToBottom();
}
}
var self = {
init: function(chatHistoryBlock, initialUserInfo) {
ensureChatDay(+new Date); // so that current date shows up right away
$("a#chatloadmore").click(self.loadMoreHistory);
self.handleUserJoinOrUpdate(initialUserInfo);
receiveChatHistoryBlock(chatHistoryBlock);
padutils.bindEnterAndEscape($("#chatentrybox"), function(evt) {
// return/enter
sendChat();
}, null);
self.scrollToBottom();
},
receiveChat: function(msg) {
var box = $("#chatlines").get(0);
var wasAtBottom = (box.scrollTop -
(box.scrollHeight - $(box).height()) >= -5);
addChatLine(msg.userId, +new Date, msg.senderName, msg.lineText, false);
if (wasAtBottom) {
window.setTimeout(function() {
self.scrollToBottom();
}, 0);
}
},
handleUserJoinOrUpdate: function(userInfo) {
changeAuthorColorData(userInfo.userId,
{ colorId: userInfo.colorId, faded: false });
},
handleUserLeave: function(userInfo) {
changeAuthorColorData(userInfo.userId,
{ colorId: userInfo.colorId, faded: true });
},
scrollToBottom: function() {
var box = $("#chatlines").get(0);
box.scrollTop = box.scrollHeight;
},
scrollToTop: function() {
var box = $("#chatlines").get(0);
box.scrollTop = 0;
},
loadMoreHistory: function() {
if (loadingMoreHistory) {
return;
}
var end = oldestHistoricalLine;
var start = Math.max(0, end - HISTORY_LINES_TO_LOAD_AT_A_TIME);
var padId = pad.getPadId();
loadingMoreHistory = true;
$("#padchat #chatloadmore").css('display', 'none');
$("#padchat #chatloadingmore").css('display', 'block');
$.ajax({
type: 'get',
url: '/ep/pad/chathistory',
data: { padId: padId, start: start, end: end },
success: success,
error: error
});
function success(text) {
notLoading();
var result = JSON.parse(text);
// try to keep scrolled to the same place...
var scrollBox = $("#chatlines").get(0);
var scrollDeterminer = function() { return 0; };
var topLine = $("#chatlines .chatday:first .chatline:first").children().eq(0);
if (topLine.length > 0) {
var posTop = topLine.position().top;
var scrollTop = scrollBox.scrollTop;
scrollDeterminer = function() {
var newPosTop = topLine.position().top;
return newPosTop + (scrollTop - posTop);
};
}
receiveChatHistoryBlock(result);
scrollBox.scrollTop = Math.max(0, Math.min(scrollBox.scrollHeight, scrollDeterminer()));
}
function error() {
notLoading();
}
function notLoading() {
loadingMoreHistory = false;
$("#padchat #chatloadmore").css('display', 'block');
$("#padchat #chatloadingmore").css('display', 'none');
}
}
};
return self;
}());

View file

@ -0,0 +1,53 @@
/**
* 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 padconnectionstatus = (function() {
var status = {what: 'connecting'};
var self = {
init: function() {
$('button#forcereconnect').click(function() {
pad.forceReconnect();
});
},
connected: function() {
status = {what: 'connected'};
padmodals.hideModal(500);
},
reconnecting: function() {
status = {what: 'reconnecting'};
$("#connectionbox").get(0).className = 'modaldialog cboxreconnecting';
padmodals.showModal("#connectionbox", 500);
},
disconnected: function(msg) {
status = {what: 'disconnected', why: msg};
var k = String(msg).toLowerCase(); // known reason why
if (!(k == 'userdup' || k == 'looping' || k == 'slowcommit' ||
k == 'initsocketfail' || k == 'unauth')) {
k = 'unknown';
}
var cls = 'modaldialog cboxdisconnected cboxdisconnected_'+k;
$("#connectionbox").get(0).className = cls;
padmodals.showModal("#connectionbox", 500);
},
isFullyConnected: function() {
return status.what == 'connected';
},
getStatus: function() { return status; }
};
return self;
}());

101
static/js/pad_cookie.js Normal file
View file

@ -0,0 +1,101 @@
/**
* 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 padcookie = (function(){
function getRawCookie() {
// returns null if can't get cookie text
if (! document.cookie) {
return null;
}
// look for (start of string OR semicolon) followed by whitespace followed by prefs=(something);
var regexResult = document.cookie.match(/(?:^|;)\s*prefs=([^;]*)(?:;|$)/);
if ((! regexResult) || (! regexResult[1])) {
return null;
}
return regexResult[1];
}
function setRawCookie(safeText) {
var expiresDate = new Date();
expiresDate.setFullYear(3000);
document.cookie = ('prefs='+safeText+';expires='+expiresDate.toGMTString());
}
function parseCookie(text) {
// returns null if can't parse cookie.
try {
var cookieData = JSON.parse(unescape(text));
return cookieData;
}
catch (e) {
return null;
}
}
function stringifyCookie(data) {
return escape(JSON.stringify(data));
}
function saveCookie() {
if (! inited) {
return;
}
setRawCookie(stringifyCookie(cookieData));
if (pad.getIsProPad() && (! getRawCookie()) && (! alreadyWarnedAboutNoCookies)) {
alert("Warning: it appears that your browser does not have cookies enabled."+
" EtherPad uses cookies to keep track of unique users for the purpose"+
" of putting a quota on the number of active users. Using EtherPad without "+
" cookies may fill up your server's user quota faster than expected.");
alreadyWarnedAboutNoCookies = true;
}
}
var wasNoCookie = true;
var cookieData = {};
var alreadyWarnedAboutNoCookies = false;
var inited = false;
var self = {
init: function(prefsToSet) {
var rawCookie = getRawCookie();
if (rawCookie) {
var cookieObj = parseCookie(rawCookie);
if (cookieObj) {
wasNoCookie = false; // there was a cookie
delete cookieObj.userId;
delete cookieObj.name;
delete cookieObj.colorId;
cookieData = cookieObj;
}
}
for(var k in prefsToSet) {
cookieData[k] = prefsToSet[k];
}
inited = true;
saveCookie();
},
wasNoCookie: function() { return wasNoCookie; },
getPref: function(prefName) {
return cookieData[prefName];
},
setPref: function(prefName, value) {
cookieData[prefName] = value;
saveCookie();
}
};
return self;
}());

347
static/js/pad_docbar.js Normal file
View file

@ -0,0 +1,347 @@
/**
* 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 paddocbar = (function() {
var isTitleEditable = false;
var isEditingTitle = false;
var isEditingPassword = false;
var enabled = false;
function getPanelOpenCloseAnimator(panelName, panelHeight) {
var wrapper = $("#"+panelName+"-wrapper");
var openingClass = "docbar"+panelName+"-opening";
var openClass = "docbar"+panelName+"-open";
var closingClass = "docbar"+panelName+"-closing";
function setPanelState(action) {
$("#docbar").removeClass(openingClass).removeClass(openClass).
removeClass(closingClass);
if (action != "closed") {
$("#docbar").addClass("docbar"+panelName+"-"+action);
}
}
function openCloseAnimate(state) {
function pow(x) { x = 1-x; x *= x*x; return 1-x; }
if (state == -1) {
// startng to open
setPanelState("opening");
wrapper.css('height', '0');
}
else if (state < 0) {
// opening
var height = Math.round(pow(state+1)*(panelHeight-1))+'px';
wrapper.css('height', height);
}
else if (state == 0) {
// open
setPanelState("open");
wrapper.css('height', panelHeight-1);
}
else if (state < 1) {
// closing
setPanelState("closing");
var height = Math.round((1-pow(state))*(panelHeight-1))+'px';
wrapper.css('height', height);
}
else if (state == 1) {
// closed
setPanelState("closed");
wrapper.css('height', '0');
}
}
return padutils.makeShowHideAnimator(openCloseAnimate, false, 25, 500);
}
var currentPanel = null;
function setCurrentPanel(newCurrentPanel) {
if (currentPanel != newCurrentPanel) {
currentPanel = newCurrentPanel;
padutils.cancelActions("hide-docbar-panel");
}
}
var panels;
function changePassword(newPass) {
if ((newPass || null) != (self.password || null)) {
self.password = (newPass || null);
pad.notifyChangePassword(newPass);
}
self.renderPassword();
}
var self = {
title: null,
password: null,
init: function(opts) {
panels = {
impexp: { animator: getPanelOpenCloseAnimator("impexp", 160) },
savedrevs: { animator: getPanelOpenCloseAnimator("savedrevs", 79) },
options: { animator: getPanelOpenCloseAnimator(
"options", 114) },
security: { animator: getPanelOpenCloseAnimator("security", 130) }
};
isTitleEditable = opts.isTitleEditable;
self.title = opts.initialTitle;
self.password = opts.initialPassword;
$("#docbarimpexp").click(function() {self.togglePanel("impexp");});
$("#docbarsavedrevs").click(function() {self.togglePanel("savedrevs");});
$("#docbaroptions").click(function() {self.togglePanel("options");});
$("#docbarsecurity").click(function() {self.togglePanel("security");});
$("#docbarrenamelink").click(self.editTitle);
$("#padtitlesave").click(function() { self.closeTitleEdit(true); });
$("#padtitlecancel").click(function() { self.closeTitleEdit(false); });
padutils.bindEnterAndEscape($("#padtitleedit"),
function() {
$("#padtitlesave").trigger('click'); },
function() {
$("#padtitlecancel").trigger('click'); });
$("#options-close").click(function() {self.setShownPanel(null);});
$("#security-close").click(function() {self.setShownPanel(null);});
if (pad.getIsProPad()) {
self.initPassword();
}
enabled = true;
self.render();
// public/private
$("#security-access input").bind("change click", function(evt) {
pad.changePadOption('guestPolicy',
$("#security-access input[name='padaccess']:checked").val());
});
self.setGuestPolicy(opts.guestPolicy);
},
setGuestPolicy: function(newPolicy) {
$("#security-access input[value='"+newPolicy+"']").attr("checked",
"checked");
self.render();
},
initPassword: function() {
self.renderPassword();
$("#password-clearlink").click(function() {
changePassword(null);
});
$("#password-setlink, #password-display").click(function() {
self.enterPassword();
});
$("#password-cancellink").click(function() {
self.exitPassword(false);
});
$("#password-savelink").click(function() {
self.exitPassword(true);
});
padutils.bindEnterAndEscape($("#security-passwordedit"),
function() {
self.exitPassword(true);
},
function() {
self.exitPassword(false);
});
},
enterPassword: function() {
isEditingPassword = true;
$("#security-passwordedit").val(self.password || '');
self.renderPassword();
$("#security-passwordedit").focus().select();
},
exitPassword: function(accept) {
isEditingPassword = false;
if (accept) {
changePassword($("#security-passwordedit").val());
}
else {
self.renderPassword();
}
},
renderPassword: function() {
if (isEditingPassword) {
$("#password-nonedit").hide();
$("#password-inedit").show();
}
else {
$("#password-nonedit").toggleClass('nopassword', ! self.password);
$("#password-setlink").html(self.password ? "Change..." : "Set...");
if (self.password) {
$("#password-display").html(self.password.replace(/./g, '&#8226;'));
}
else {
$("#password-display").html("None");
}
$("#password-inedit").hide();
$("#password-nonedit").show();
}
},
togglePanel: function(panelName) {
if (panelName in panels) {
if (currentPanel == panelName) {
self.setShownPanel(null);
}
else {
self.setShownPanel(panelName);
}
}
},
setShownPanel: function(panelName) {
function animateHidePanel(panelName, next) {
var delay = 0;
if (panelName == 'options' && isEditingPassword) {
// give user feedback that the password they've
// typed in won't actually take effect
self.exitPassword(false);
delay = 500;
}
window.setTimeout(function() {
panels[panelName].animator.hide();
if (next) {
next();
}
}, delay);
}
if (! panelName) {
if (currentPanel) {
animateHidePanel(currentPanel);
setCurrentPanel(null);
}
}
else if (panelName in panels) {
if (currentPanel != panelName) {
if (currentPanel) {
animateHidePanel(currentPanel, function() {
panels[panelName].animator.show();
setCurrentPanel(panelName);
});
}
else {
panels[panelName].animator.show();
setCurrentPanel(panelName);
}
}
}
},
isPanelShown: function(panelName) {
if (! panelName) {
return ! currentPanel;
}
else {
return (panelName == currentPanel);
}
},
changeTitle: function(newTitle) {
self.title = newTitle;
self.render();
},
editTitle: function() {
if (! enabled) {
return;
}
$("#padtitleedit").val(self.title);
isEditingTitle = true;
self.render();
$("#padtitleedit").focus().select();
},
closeTitleEdit: function(accept) {
if (! enabled) {
return;
}
if (accept) {
var newTitle = $("#padtitleedit").val();
if (newTitle) {
newTitle = newTitle.substring(0, 80);
self.title = newTitle;
pad.notifyChangeTitle(newTitle);
}
}
isEditingTitle = false;
self.render();
},
changePassword: function(newPass) {
if (newPass) {
self.password = newPass;
}
else {
self.password = null;
}
self.renderPassword();
},
render: function() {
if (isEditingTitle) {
$("#docbarpadtitle").hide();
$("#docbarrenamelink").hide();
$("#padtitleedit").show();
$("#padtitlebuttons").show();
if (! enabled) {
$("#padtitleedit").attr('disabled', 'disabled');
}
else {
$("#padtitleedit").removeAttr('disabled');
}
}
else {
$("#padtitleedit").hide();
$("#padtitlebuttons").hide();
var titleSpan = $("#docbarpadtitle span");
titleSpan.html(padutils.escapeHtml(self.title));
$("#docbarpadtitle").attr('title',
(pad.isPadPublic() ? "Public Pad: " : "")+
self.title);
$("#docbarpadtitle").show();
if (isTitleEditable) {
var titleRight = $("#docbarpadtitle").position().left +
$("#docbarpadtitle span").position().left +
Math.min($("#docbarpadtitle").width(),
$("#docbarpadtitle span").width());
$("#docbarrenamelink").css('left', titleRight + 10).show();
}
if (pad.isPadPublic()) {
$("#docbar").addClass("docbar-public");
}
else {
$("#docbar").removeClass("docbar-public");
}
}
},
disable: function() {
enabled = false;
self.render();
},
handleResizePage: function() {
padsavedrevs.handleResizePage();
},
hideLaterIfNoOtherInteraction: function() {
return padutils.getCancellableAction('hide-docbar-panel',
function() {
self.setShownPanel(null);
});
}
};
return self;
}());

122
static/js/pad_editbar.js Normal file
View file

@ -0,0 +1,122 @@
/**
* 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 padeditbar = (function(){
var syncAnimation = (function() {
var SYNCING = -100;
var DONE = 100;
var state = DONE;
var fps = 25;
var step = 1/fps;
var T_START = -0.5;
var T_FADE = 1.0;
var T_GONE = 1.5;
var animator = padutils.makeAnimationScheduler(function() {
if (state == SYNCING || state == DONE) {
return false;
}
else if (state >= T_GONE) {
state = DONE;
$("#syncstatussyncing").css('display', 'none');
$("#syncstatusdone").css('display', 'none');
return false;
}
else if (state < 0) {
state += step;
if (state >= 0) {
$("#syncstatussyncing").css('display', 'none');
$("#syncstatusdone").css('display', 'block').css('opacity', 1);
}
return true;
}
else {
state += step;
if (state >= T_FADE) {
$("#syncstatusdone").css('opacity', (T_GONE - state) / (T_GONE - T_FADE));
}
return true;
}
}, step*1000);
return {
syncing: function() {
state = SYNCING;
$("#syncstatussyncing").css('display', 'block');
$("#syncstatusdone").css('display', 'none');
},
done: function() {
state = T_START;
animator.scheduleAnimation();
}
};
}());
var self = {
init: function() {
$("#editbar .editbarbutton").attr("unselectable", "on"); // for IE
$("#editbar").removeClass("disabledtoolbar").addClass("enabledtoolbar");
},
isEnabled: function() {
return ! $("#editbar").hasClass('disabledtoolbar');
},
disable: function() {
$("#editbar").addClass('disabledtoolbar').removeClass("enabledtoolbar");
},
toolbarClick: function(cmd) {
if (self.isEnabled()) {
if (cmd == 'save') {
padsavedrevs.saveNow();
} else {
padeditor.ace.callWithAce(function (ace) {
if (cmd == 'bold' || cmd == 'italic' || cmd == 'underline' || cmd == 'strikethrough')
ace.ace_toggleAttributeOnSelection(cmd);
else if (cmd == 'undo' || cmd == 'redo')
ace.ace_doUndoRedo(cmd);
else if (cmd == 'insertunorderedlist')
ace.ace_doInsertUnorderedList();
else if (cmd == 'indent') {
if (! ace.ace_doIndentOutdent(false)) {
ace.ace_doInsertUnorderedList();
}
} else if (cmd == 'outdent') {
ace.ace_doIndentOutdent(true);
} else if (cmd == 'clearauthorship') {
if ((!(ace.ace_getRep().selStart && ace.ace_getRep().selEnd)) || ace.ace_isCaret()) {
if (window.confirm("Clear authorship colors on entire document?")) {
ace.ace_performDocumentApplyAttributesToCharRange(0, ace.ace_getRep().alltext.length,
[['author', '']]);
}
} else {
ace.ace_setAttributeOnSelection('author', '');
}
}
}, cmd, true);
}
}
padeditor.ace.focus();
},
setSyncStatus: function(status) {
if (status == "syncing") {
syncAnimation.syncing();
}
else if (status == "done") {
syncAnimation.done();
}
}
};
return self;
}());

124
static/js/pad_editor.js Normal file
View file

@ -0,0 +1,124 @@
/**
* 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 padeditor = (function(){
var self = {
ace: null, // this is accessed directly from other files
viewZoom: 100,
init: function(readyFunc, initialViewOptions) {
function aceReady() {
$("#editorloadingbox").hide();
if (readyFunc) {
readyFunc();
}
}
self.ace = new Ace2Editor();
self.ace.init("editorcontainer", "", aceReady);
self.ace.setProperty("wraps", true);
if (pad.getIsDebugEnabled()) {
self.ace.setProperty("dmesg", pad.dmesg);
}
self.initViewOptions();
self.setViewOptions(initialViewOptions);
// view bar
self.initViewZoom();
$("#viewbarcontents").show();
},
initViewOptions: function() {
padutils.bindCheckboxChange($("#options-linenoscheck"), function() {
pad.changeViewOption('showLineNumbers',
padutils.getCheckbox($("#options-linenoscheck")));
});
padutils.bindCheckboxChange($("#options-colorscheck"), function() {
pad.changeViewOption('showAuthorColors',
padutils.getCheckbox("#options-colorscheck"));
});
$("#viewfontmenu").change(function() {
pad.changeViewOption('useMonospaceFont',
$("#viewfontmenu").val() == 'monospace');
});
},
setViewOptions: function(newOptions) {
function getOption(key, defaultValue) {
var value = String(newOptions[key]);
if (value == "true") return true;
if (value == "false") return false;
return defaultValue;
}
var v;
v = getOption('showLineNumbers', true);
self.ace.setProperty("showslinenumbers", v);
padutils.setCheckbox($("#options-linenoscheck"), v);
v = getOption('showAuthorColors', true);
self.ace.setProperty("showsauthorcolors", v);
padutils.setCheckbox($("#options-colorscheck"), v);
v = getOption('useMonospaceFont', false);
self.ace.setProperty("textface",
(v ? "monospace" : "Arial, sans-serif"));
$("#viewfontmenu").val(v ? "monospace" : "normal");
},
initViewZoom: function() {
var viewZoom = Number(padcookie.getPref('viewZoom'));
if ((! viewZoom) || isNaN(viewZoom)) {
viewZoom = 100;
}
self.setViewZoom(viewZoom);
$("#viewzoommenu").change(function(evt) {
// strip initial 'z' from val
self.setViewZoom(Number($("#viewzoommenu").val().substring(1)));
});
},
setViewZoom: function(percent) {
if (! (percent >= 50 && percent <= 1000)) {
// percent is out of sane range or NaN (which fails comparisons)
return;
}
self.viewZoom = percent;
$("#viewzoommenu").val('z'+percent);
var baseSize = 13;
self.ace.setProperty('textsize',
Math.round(baseSize * self.viewZoom / 100));
padcookie.setPref('viewZoom', percent);
},
dispose: function() {
if (self.ace) {
self.ace.destroy();
}
},
disable: function() {
if (self.ace) {
self.ace.setProperty("grayedOut", true);
self.ace.setEditable(false);
}
},
restoreRevisionText: function(dataFromServer) {
pad.addHistoricalAuthors(dataFromServer.historicalAuthorData);
self.ace.importAText(dataFromServer.atext, dataFromServer.apool, true);
}
};
return self;
}());

187
static/js/pad_impexp.js Normal file
View file

@ -0,0 +1,187 @@
/**
* 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 padimpexp = (function() {
///// import
var currentImportTimer = null;
var hidePanelCall = null;
function addImportFrames() {
$("#impexp-import .importframe").remove();
$('#impexp-import').append(
$('<iframe style="display: none;" name="importiframe" class="importframe"></iframe>'));
}
function fileInputUpdated() {
$('#importformfilediv').addClass('importformenabled');
$('#importsubmitinput').removeAttr('disabled');
$('#importmessagefail').fadeOut("fast");
$('#importarrow').show();
$('#importarrow').animate({paddingLeft:"0px"}, 500)
.animate({paddingLeft:"10px"}, 150, 'swing')
.animate({paddingLeft:"0px"}, 150, 'swing')
.animate({paddingLeft:"10px"}, 150, 'swing')
.animate({paddingLeft:"0px"}, 150, 'swing')
.animate({paddingLeft:"10px"}, 150, 'swing')
.animate({paddingLeft:"0px"}, 150, 'swing');
}
function fileInputSubmit() {
$('#importmessagefail').fadeOut("fast");
var ret = window.confirm(
"Importing a file will overwrite the current text of the pad."+
" Are you sure you want to proceed?");
if (ret) {
hidePanelCall = paddocbar.hideLaterIfNoOtherInteraction();
currentImportTimer = window.setTimeout(function() {
if (! currentImportTimer) {
return;
}
currentImportTimer = null;
importFailed("Request timed out.");
}, 25000); // time out after some number of seconds
$('#importsubmitinput').attr({disabled: true}).val("Importing...");
window.setTimeout(function() {
$('#importfileinput').attr({disabled: true}); }, 0);
$('#importarrow').stop(true, true).hide();
$('#importstatusball').show();
}
return ret;
}
function importFailed(msg) {
importErrorMessage(msg);
importDone();
addImportFrames();
}
function importDone() {
$('#importsubmitinput').removeAttr('disabled').val("Import Now");
window.setTimeout(function() {
$('#importfileinput').removeAttr('disabled'); }, 0);
$('#importstatusball').hide();
importClearTimeout();
}
function importClearTimeout() {
if (currentImportTimer) {
window.clearTimeout(currentImportTimer);
currentImportTimer = null;
}
}
function importErrorMessage(msg) {
function showError(fade) {
$('#importmessagefail').html(
'<strong style="color: red">Import failed:</strong> '+
(msg || 'Please try a different file.'))[(fade?"fadeIn":"show")]();
}
if ($('#importexport .importmessage').is(':visible')) {
$('#importmessagesuccess').fadeOut("fast");
$('#importmessagefail').fadeOut("fast", function() {
showError(true); });
} else {
showError();
}
}
function importSuccessful(token) {
$.ajax({
type: 'post',
url: '/ep/pad/impexp/import2',
data: {token: token, padId: pad.getPadId()},
success: importApplicationSuccessful,
error: importApplicationFailed,
timeout: 25000
});
addImportFrames();
}
function importApplicationFailed(xhr, textStatus, errorThrown) {
importErrorMessage("Error during conversion.");
importDone();
}
function importApplicationSuccessful(data, textStatus) {
if (data.substr(0, 2) == "ok") {
if ($('#importexport .importmessage').is(':visible')) {
$('#importexport .importmessage').hide();
}
$('#importmessagesuccess').html(
'<strong style="color: green">Import successful!</strong>').show();
$('#importformfilediv').hide();
window.setTimeout(function() {
$('#importmessagesuccess').fadeOut("slow", function() {
$('#importformfilediv').show();
});
if (hidePanelCall) {
hidePanelCall();
}
}, 3000);
} else if (data.substr(0, 4) == "fail") {
importErrorMessage(
"Couldn't update pad contents. This can happen if your web browser has \"cookies\" disabled.");
} else if (data.substr(0, 4) == "msg:") {
importErrorMessage(data.substr(4));
}
importDone();
}
///// export
function cantExport() {
var type = $(this);
if (type.hasClass("exporthrefpdf")) {
type = "PDF";
} else if (type.hasClass("exporthrefdoc")) {
type = "Microsoft Word";
} else if (type.hasClass("exporthrefodt")) {
type = "OpenDocument";
} else {
type = "this file";
}
alert("Exporting as "+type+" format is disabled. Please contact your"+
" system administrator for details.");
return false;
}
/////
var self = {
init: function() {
$("#impexp-close").click(function() {paddocbar.setShownPanel(null);});
addImportFrames();
$("#importfileinput").change(fileInputUpdated);
$('#importform').submit(fileInputSubmit);
$('.disabledexport').click(cantExport);
},
handleFrameCall: function(callName, argsArray) {
if (callName == 'importFailed') {
importFailed(argsArray[0]);
}
else if (callName == 'importSuccessful') {
importSuccessful(argsArray[0]);
}
},
disable: function() {
$("#impexp-disabled-clickcatcher").show();
$("#impexp-import").css('opacity', 0.5);
$("#impexp-export").css('opacity', 0.5);
},
enable: function() {
$("#impexp-disabled-clickcatcher").hide();
$("#impexp-import").css('opacity', 1);
$("#impexp-export").css('opacity', 1);
}
};
return self;
}());

306
static/js/pad_modals.js Normal file
View file

@ -0,0 +1,306 @@
/**
* 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 padmodals = (function() {
/*var clearFeedbackEmail = function() {};
function clearFeedback() {
clearFeedbackEmail();
$("#feedbackbox-message").val('');
}
var sendingFeedback = false;
function setSendingFeedback(v) {
v = !! v;
if (sendingFeedback != v) {
sendingFeedback = v;
if (v) {
$("#feedbackbox-send").css('opacity', 0.75);
}
else {
$("#feedbackbox-send").css('opacity', 1);
}
}
}*/
var sendingInvite = false;
function setSendingInvite(v) {
v = !! v;
if (sendingInvite != v) {
sendingInvite = v;
if (v) {
$(".sharebox-send").css('opacity', 0.75);
}
else {
$("#sharebox-send").css('opacity', 1);
}
}
}
var clearShareBoxTo = function() {};
function clearShareBox() {
clearShareBoxTo();
}
var self = {
init: function() {
self.initFeedback();
self.initShareBox();
},
initFeedback: function() {
/*var emailField = $("#feedbackbox-email");
clearFeedbackEmail =
padutils.makeFieldLabeledWhenEmpty(emailField, '(your email address)').clear;
clearFeedback();*/
$("#feedbackbox-hide").click(function() {
self.hideModal();
});
/*$("#feedbackbox-send").click(function() {
self.sendFeedbackEmail();
});*/
$("#feedbackbutton").click(function() {
self.showFeedback();
});
$("#uservoicelinks a").click(function() {
self.hideModal();
return true;
});
$("#feedbackemails a").each(function() {
var node = $(this);
node.attr('href', "mailto:"+node.attr('href')+"@etherpad.com");
});
},
initShareBox: function() {
$("#sharebutton, #nootherusers a").click(function() {
self.showShareBox();
});
$("#sharebox-hide").click(function() {
self.hideModal();
});
$("#sharebox-send").click(function() {
self.sendInvite();
});
$("#sharebox-url").click(function() {
$("#sharebox-url").focus().select();
});
clearShareBoxTo =
padutils.makeFieldLabeledWhenEmpty($("#sharebox-to"),
"(email addresses)").clear;
clearShareBox();
$("#sharebox-subject").val(self.getDefaultShareBoxSubjectForName(pad.getUserName()));
$("#sharebox-message").val(self.getDefaultShareBoxMessageForName(pad.getUserName()));
$("#sharebox-stripe .setsecurity").click(function() {
self.hideModal();
paddocbar.setShownPanel('security');
});
},
getDefaultShareBoxMessageForName: function(name) {
return (name || "Somebody")+" has shared an EtherPad document with you."+
"\n\n"+"View it here:\n\n"+
padutils.escapeHtml($(".sharebox-url").val()+"\n");
},
getDefaultShareBoxSubjectForName: function(name) {
return (name || "Somebody")+" invited you to an EtherPad document";
},
relayoutWithBottom: function(px) {
$("#modaloverlay").height(px);
$("#sharebox").css('left',
Math.floor(($(window).width() -
$("#sharebox").outerWidth())/2));
$("#feedbackbox").css('left',
Math.floor(($(window).width() -
$("#feedbackbox").outerWidth())/2));
},
showFeedback: function() {
self.showModal("#feedbackbox");
},
showShareBox: function() {
// when showing the dialog, if it still says "Somebody" invited you
// then we fill in the updated username if there is one;
// otherwise, we don't touch it, perhaps the user is happy with it
var msgbox = $("#sharebox-message");
if (msgbox.val() == self.getDefaultShareBoxMessageForName(null)) {
msgbox.val(self.getDefaultShareBoxMessageForName(pad.getUserName()));
}
var subjBox = $("#sharebox-subject");
if (subjBox.val() == self.getDefaultShareBoxSubjectForName(null)) {
subjBox.val(self.getDefaultShareBoxSubjectForName(pad.getUserName()));
}
if (pad.isPadPublic()) {
$("#sharebox-stripe").get(0).className = 'sharebox-stripe-public';
}
else {
$("#sharebox-stripe").get(0).className = 'sharebox-stripe-private';
}
self.showModal("#sharebox", 500);
$("#sharebox-url").focus().select();
},
showModal: function(modalId, duration) {
$(".modaldialog").hide();
$(modalId).show().css({'opacity': 0}).animate({'opacity': 1}, duration);
$("#modaloverlay").show().css({'opacity': 0}).animate({'opacity': 1}, duration);
},
hideModal: function(duration) {
padutils.cancelActions('hide-feedbackbox');
padutils.cancelActions('hide-sharebox');
$("#sharebox-response").hide();
$(".modaldialog").animate({'opacity': 0}, duration, function () { $("#modaloverlay").hide(); });
$("#modaloverlay").animate({'opacity': 0}, duration, function () { $("#modaloverlay").hide(); });
},
hideFeedbackLaterIfNoOtherInteraction: function() {
return padutils.getCancellableAction('hide-feedbackbox',
function() {
self.hideModal();
});
},
hideShareboxLaterIfNoOtherInteraction: function() {
return padutils.getCancellableAction('hide-sharebox',
function() {
self.hideModal();
});
},
/* sendFeedbackEmail: function() {
if (sendingFeedback) {
return;
}
var message = $("#feedbackbox-message").val();
if (! message) {
return;
}
var email = ($("#feedbackbox-email").hasClass('editempty') ? '' :
$("#feedbackbox-email").val());
var padId = pad.getPadId();
var username = pad.getUserName();
setSendingFeedback(true);
$("#feedbackbox-response").html("Sending...").get(0).className = '';
$("#feedbackbox-response").show();
$.ajax({
type: 'post',
url: '/ep/pad/feedback',
data: {
feedback: message,
padId: padId,
username: username,
email: email
},
success: success,
error: error
});
var hideCall = self.hideFeedbackLaterIfNoOtherInteraction();
function success(msg) {
setSendingFeedback(false);
clearFeedback();
$("#feedbackbox-response").html("Thanks for your feedback").get(0).className = 'goodresponse';
$("#feedbackbox-response").show();
window.setTimeout(function() {
$("#feedbackbox-response").fadeOut('slow', function() {
hideCall();
});
}, 1500);
}
function error(e) {
setSendingFeedback(false);
$("#feedbackbox-response").html("Could not send feedback. Please email us at feedback"+"@"+"etherpad.com instead.").get(0).className = 'badresponse';
$("#feedbackbox-response").show();
}
},*/
sendInvite: function() {
if (sendingInvite) {
return;
}
if (! pad.isFullyConnected()) {
displayErrorMessage("Error: Connection to the server is down or flaky.");
return;
}
var message = $("#sharebox-message").val();
if (! message) {
displayErrorMessage("Please enter a message body before sending.");
return;
}
var emails = ($("#sharebox-to").hasClass('editempty') ? '' :
$("#sharebox-to").val()) || '';
// find runs of characters that aren't obviously non-email punctuation
var emailArray = emails.match(/[^\s,:;<>\"\'\/\(\)\[\]{}]+/g) || [];
if (emailArray.length == 0) {
displayErrorMessage('Please enter at least one "To:" address.');
$("#sharebox-to").focus().select();
return;
}
for(var i=0;i<emailArray.length;i++) {
var addr = emailArray[i];
if (! addr.match(/^[\w\.\_\+\-]+\@[\w\_\-]+\.[\w\_\-\.]+$/)) {
displayErrorMessage('"'+padutils.escapeHtml(addr) +
'" does not appear to be a valid email address.');
return;
}
}
var subject = $("#sharebox-subject").val();
if (! subject) {
subject = self.getDefaultShareBoxSubjectForName(pad.getUserName());
$("#sharebox-subject").val(subject); // force the default subject
}
var padId = pad.getPadId();
var username = pad.getUserName();
setSendingInvite(true);
$("#sharebox-response").html("Sending...").get(0).className = '';
$("#sharebox-response").show();
$.ajax({
type: 'post',
url: '/ep/pad/emailinvite',
data: {
message: message,
toEmails: emailArray.join(','),
subject: subject,
username: username,
padId: padId
},
success: success,
error: error
});
var hideCall = self.hideShareboxLaterIfNoOtherInteraction();
function success(msg) {
setSendingInvite(false);
$("#sharebox-response").html("Email invitation sent!").get(0).className = 'goodresponse';
$("#sharebox-response").show();
window.setTimeout(function() {
$("#sharebox-response").fadeOut('slow', function() {
hideCall();
});
}, 1500);
}
function error(e) {
setSendingFeedback(false);
$("#sharebox-response").html("An error occurred; no email was sent.").get(0).className = 'badresponse';
$("#sharebox-response").show();
}
function displayErrorMessage(msgHtml) {
$("#sharebox-response").html(msgHtml).get(0).className = 'badresponse';
$("#sharebox-response").show();
}
}
};
return self;
}());

408
static/js/pad_savedrevs.js Normal file
View file

@ -0,0 +1,408 @@
/**
* 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 padsavedrevs = (function() {
function reversedCopy(L) {
var L2 = L.slice();
L2.reverse();
return L2;
}
function makeRevisionBox(revisionInfo, rnum) {
var box = $('<div class="srouterbox">'+
'<div class="srinnerbox">'+
'<a href="javascript:void(0)" class="srname"><!-- --></a>'+
'<div class="sractions"><a class="srview" href="javascript:void(0)" target="_blank">view</a> | <a class="srrestore" href="javascript:void(0)">restore</a></div>'+
'<div class="srtime"><!-- --></div>'+
'<div class="srauthor"><!-- --></div>'+
'<img class="srtwirly" src="/static/img/misc/status-ball.gif">'+
'</div></div>');
setBoxLabel(box, revisionInfo.label);
setBoxTimestamp(box, revisionInfo.timestamp);
box.find(".srauthor").html("by "+padutils.escapeHtml(revisionInfo.savedBy));
var viewLink = '/ep/pad/view/'+pad.getPadId()+'/'+revisionInfo.id;
box.find(".srview").attr('href', viewLink);
var restoreLink = 'javascript:void padsavedrevs.restoreRevision('+rnum+');';
box.find(".srrestore").attr('href', restoreLink);
box.find(".srname").click(function(evt) {
editRevisionLabel(rnum, box);
});
return box;
}
function setBoxLabel(box, label) {
box.find(".srname").html(padutils.escapeHtml(label)).attr('title', label);
}
function setBoxTimestamp(box, timestamp) {
box.find(".srtime").html(padutils.escapeHtml(
padutils.timediff(new Date(timestamp))));
}
function getNthBox(n) {
return $("#savedrevisions .srouterbox").eq(n);
}
function editRevisionLabel(rnum, box) {
var input = $('<input type="text" class="srnameedit"/>');
box.find(".srnameedit").remove(); // just in case
var label = box.find(".srname");
input.width(label.width());
input.height(label.height());
input.css('top', label.position().top);
input.css('left', label.position().left);
label.after(input);
label.css('opacity', 0);
function endEdit() {
input.remove();
label.css('opacity', 1);
}
var rev = currentRevisionList[rnum];
var oldLabel = rev.label;
input.blur(function() {
var newLabel = input.val();
if (newLabel && newLabel != oldLabel) {
relabelRevision(rnum, newLabel);
}
endEdit();
});
input.val(rev.label).focus().select();
padutils.bindEnterAndEscape(input, function onEnter() {
input.blur();
}, function onEscape() {
input.val('').blur();
});
}
function relabelRevision(rnum, newLabel) {
var rev = currentRevisionList[rnum];
$.ajax({
type: 'post',
url: '/ep/pad/saverevisionlabel',
data: {userId: pad.getUserId(),
padId: pad.getPadId(),
revId: rev.id,
newLabel: newLabel},
success: success,
error: error
});
function success(text) {
var newRevisionList = JSON.parse(text);
self.newRevisionList(newRevisionList);
pad.sendClientMessage({
type: 'revisionLabel',
revisionList: reversedCopy(currentRevisionList),
savedBy: pad.getUserName(),
newLabel: newLabel
});
}
function error(e) {
alert("Oops! There was an error saving that revision label. Please try again later.");
}
}
var currentRevisionList = [];
function setRevisionList(newRevisionList, noAnimation) {
// deals with changed labels and new added revisions
for(var i=0; i<currentRevisionList.length; i++) {
var a = currentRevisionList[i];
var b = newRevisionList[i];
if (b.label != a.label) {
setBoxLabel(getNthBox(i), b.label);
}
}
for(var j=currentRevisionList.length; j<newRevisionList.length; j++) {
var newBox = makeRevisionBox(newRevisionList[j], j);
$("#savedrevs-scrollinner").append(newBox);
newBox.css('left', j * REVISION_BOX_WIDTH);
}
var newOnes = (newRevisionList.length > currentRevisionList.length);
currentRevisionList = newRevisionList;
if (newOnes) {
setDesiredScroll(getMaxScroll());
if (noAnimation) {
setScroll(desiredScroll);
}
if (! noAnimation) {
var nameOfLast = currentRevisionList[currentRevisionList.length-1].label;
displaySavedTip(nameOfLast);
}
}
}
function refreshRevisionList() {
for(var i=0;i<currentRevisionList.length; i++) {
var r = currentRevisionList[i];
var box = getNthBox(i);
setBoxTimestamp(box, r.timestamp);
}
}
var savedTipAnimator = padutils.makeShowHideAnimator(function(state) {
if (state == -1) {
$("#revision-notifier").css('opacity', 0).css('display', 'block');
}
else if (state == 0) {
$("#revision-notifier").css('opacity', 1);
}
else if (state == 1) {
$("#revision-notifier").css('opacity', 0).css('display', 'none');
}
else if (state < 0) {
$("#revision-notifier").css('opacity', 1);
}
else if (state > 0) {
$("#revision-notifier").css('opacity', 1 - state);
}
}, false, 25, 300);
function displaySavedTip(text) {
$("#revision-notifier .name").html(padutils.escapeHtml(text));
savedTipAnimator.show();
padutils.cancelActions("hide-revision-notifier");
var hideLater = padutils.getCancellableAction("hide-revision-notifier",
function() {
savedTipAnimator.hide();
});
window.setTimeout(hideLater, 3000);
}
var REVISION_BOX_WIDTH = 120;
var curScroll = 0; // distance between left of revisions and right of view
var desiredScroll = 0;
function getScrollWidth() {
return REVISION_BOX_WIDTH * currentRevisionList.length;
}
function getViewportWidth() {
return $("#savedrevs-scrollouter").width();
}
function getMinScroll() {
return Math.min(getViewportWidth(), getScrollWidth());
}
function getMaxScroll() {
return getScrollWidth();
}
function setScroll(newScroll) {
curScroll = newScroll;
$("#savedrevs-scrollinner").css('right', newScroll);
updateScrollArrows();
}
function setDesiredScroll(newDesiredScroll, dontUpdate) {
desiredScroll = Math.min(getMaxScroll(), Math.max(getMinScroll(),
newDesiredScroll));
if (! dontUpdate) {
updateScroll();
}
}
function updateScroll() {
updateScrollArrows();
scrollAnimator.scheduleAnimation();
}
function updateScrollArrows() {
$("#savedrevs-scrollleft").toggleClass("disabledscrollleft",
desiredScroll <= getMinScroll());
$("#savedrevs-scrollright").toggleClass("disabledscrollright",
desiredScroll >= getMaxScroll());
}
var scrollAnimator = padutils.makeAnimationScheduler(function() {
setDesiredScroll(desiredScroll, true); // re-clamp
if (Math.abs(desiredScroll - curScroll) < 1) {
setScroll(desiredScroll);
return false;
}
else {
setScroll(curScroll + (desiredScroll - curScroll)*0.5);
return true;
}
}, 50, 2);
var isSaving = false;
function setIsSaving(v) {
isSaving = v;
rerenderButton();
}
function haveReachedRevLimit() {
var mv = pad.getPrivilege('maxRevisions');
return (!(mv < 0 || mv > currentRevisionList.length));
}
function rerenderButton() {
if (isSaving || (! pad.isFullyConnected()) ||
haveReachedRevLimit()) {
$("#savedrevs-savenow").css('opacity', 0.75);
}
else {
$("#savedrevs-savenow").css('opacity', 1);
}
}
var scrollRepeatTimer = null;
var scrollStartTime = 0;
function setScrollRepeatTimer(dir) {
clearScrollRepeatTimer();
scrollStartTime = +new Date;
scrollRepeatTimer = window.setTimeout(function f() {
if (! scrollRepeatTimer) {
return;
}
self.scroll(dir);
var scrollTime = (+new Date) - scrollStartTime;
var delay = (scrollTime > 2000 ? 50 : 300);
scrollRepeatTimer = window.setTimeout(f, delay);
}, 300);
$(document).bind('mouseup', clearScrollRepeatTimer);
}
function clearScrollRepeatTimer() {
if (scrollRepeatTimer) {
window.clearTimeout(scrollRepeatTimer);
scrollRepeatTimer = null;
}
$(document).unbind('mouseup', clearScrollRepeatTimer);
}
var self = {
init: function(initialRevisions) {
self.newRevisionList(initialRevisions, true);
$("#savedrevs-savenow").click(function() { self.saveNow(); });
$("#savedrevs-scrollleft").mousedown(function() {
self.scroll('left');
setScrollRepeatTimer('left');
});
$("#savedrevs-scrollright").mousedown(function() {
self.scroll('right');
setScrollRepeatTimer('right');
});
$("#savedrevs-close").click(function() {paddocbar.setShownPanel(null);});
// update "saved n minutes ago" times
window.setInterval(function() {
refreshRevisionList();
}, 60*1000);
},
restoreRevision: function(rnum) {
var rev = currentRevisionList[rnum];
var warning = ("Restoring this revision will overwrite the current"
+ " text of the pad. "+
"Are you sure you want to continue?");
var hidePanel = paddocbar.hideLaterIfNoOtherInteraction();
var box = getNthBox(rnum);
if (confirm(warning)) {
box.find(".srtwirly").show();
$.ajax({
type: 'get',
url: '/ep/pad/getrevisionatext',
data: {padId: pad.getPadId(), revId: rev.id},
success: success,
error: error
});
}
function success(resultJson) {
untwirl();
var result = JSON.parse(resultJson);
padeditor.restoreRevisionText(result);
window.setTimeout(function() {
hidePanel();
}, 0);
}
function error(e) {
untwirl();
alert("Oops! There was an error retreiving the text (revNum= "+
rev.revNum+"; padId="+pad.getPadId());
}
function untwirl() {
box.find(".srtwirly").hide();
}
},
showReachedLimit: function() {
alert("Sorry, you do not have privileges to save more than "+
pad.getPrivilege('maxRevisions')+" revisions.");
},
newRevisionList: function(lst, noAnimation) {
// server gives us list with newest first;
// we want chronological order
var L = reversedCopy(lst);
setRevisionList(L, noAnimation);
rerenderButton();
},
saveNow: function() {
if (isSaving) {
return;
}
if (! pad.isFullyConnected()) {
return;
}
if (haveReachedRevLimit()) {
self.showReachedLimit();
return;
}
setIsSaving(true);
var savedBy = pad.getUserName() || "unnamed";
pad.callWhenNotCommitting(submitSave);
function submitSave() {
$.ajax({
type: 'post',
url: '/ep/pad/saverevision',
data: {
padId: pad.getPadId(),
savedBy: savedBy,
savedById: pad.getUserId(),
revNum: pad.getCollabRevisionNumber()
},
success: success,
error: error
});
}
function success(text) {
setIsSaving(false);
var newRevisionList = JSON.parse(text);
self.newRevisionList(newRevisionList);
pad.sendClientMessage({
type: 'newRevisionList',
revisionList: newRevisionList,
savedBy: savedBy
});
}
function error(e) {
setIsSaving(false);
alert("Oops! The server failed to save the revision. Please try again later.");
}
},
handleResizePage: function() {
updateScrollArrows();
},
handleIsFullyConnected: function(isConnected) {
rerenderButton();
},
scroll: function(dir) {
var minScroll = getMinScroll();
var maxScroll = getMaxScroll();
if (dir == 'left') {
if (desiredScroll > minScroll) {
var n = Math.floor((desiredScroll - 1 - minScroll) /
REVISION_BOX_WIDTH);
setDesiredScroll(Math.max(0, n)*REVISION_BOX_WIDTH + minScroll);
}
}
else if (dir == 'right') {
if (desiredScroll < maxScroll) {
var n = Math.floor((maxScroll - desiredScroll - 1) /
REVISION_BOX_WIDTH);
setDesiredScroll(maxScroll - Math.max(0, n)*REVISION_BOX_WIDTH);
}
}
}
};
return self;
}());

605
static/js/pad_userlist.js Normal file
View file

@ -0,0 +1,605 @@
/**
* 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 paduserlist = (function() {
var rowManager = (function() {
// The row manager handles rendering rows of the user list and animating
// their insertion, removal, and reordering. It manipulates TD height
// and TD opacity.
function nextRowId() {
return "usertr"+(nextRowId.counter++);
}
nextRowId.counter = 1;
// objects are shared; fields are "domId","data","animationStep"
var rowsFadingOut = []; // unordered set
var rowsFadingIn = []; // unordered set
var rowsPresent = []; // in order
var ANIMATION_START = -12; // just starting to fade in
var ANIMATION_END = 12; // just finishing fading out
function getAnimationHeight(step, power) {
var a = Math.abs(step/12);
if (power == 2) a = a*a;
else if (power == 3) a = a*a*a;
else if (power == 4) a = a*a*a*a;
else if (power >= 5) a = a*a*a*a*a;
return Math.round(26*(1-a));
}
var OPACITY_STEPS = 6;
var ANIMATION_STEP_TIME = 20;
var LOWER_FRAMERATE_FACTOR = 2;
var scheduleAnimation = padutils.makeAnimationScheduler(animateStep, ANIMATION_STEP_TIME,
LOWER_FRAMERATE_FACTOR).scheduleAnimation;
var NUMCOLS = 4;
// we do lots of manipulation of table rows and stuff that JQuery makes ok, despite
// IE's poor handling when manipulating the DOM directly.
function getEmptyRowHtml(height) {
return '<td colspan="'+NUMCOLS+'" style="border:0;height:'+height+'px"><!-- --></td>';
}
function isNameEditable(data) {
return (! data.name) && (data.status != 'Disconnected');
}
function replaceUserRowContents(tr, height, data) {
var tds = getUserRowHtml(height, data).match(/<td.*?<\/td>/gi);
if (isNameEditable(data) && tr.find("td.usertdname input:enabled").length > 0) {
// preserve input field node
for(var i=0; i<tds.length; i++) {
var oldTd = $(tr.find("td").get(i));
if (! oldTd.hasClass('usertdname')) {
oldTd.replaceWith(tds[i]);
}
}
}
else {
tr.html(tds.join(''));
}
return tr;
}
function getUserRowHtml(height, data) {
var nameHtml;
var isGuest = (data.id.charAt(0) != 'p');
if (data.name) {
nameHtml = padutils.escapeHtml(data.name);
if (isGuest && pad.getIsProPad()) {
nameHtml += ' (Guest)';
}
}
else {
nameHtml = '<input type="text" class="editempty newinput" value="unnamed" '+
(isNameEditable(data) ? '' : 'disabled="disabled" ')+
'/>';
}
return ['<td style="height:',height,'px" class="usertdswatch"><div class="swatch" style="background:'+data.color+'">&nbsp;</div></td>',
'<td style="height:',height,'px" class="usertdname">',nameHtml,'</td>',
'<td style="height:',height,'px" class="usertdstatus">',padutils.escapeHtml(data.status),'</td>',
'<td style="height:',height,'px" class="activity">',padutils.escapeHtml(data.activity),'</td>'].join('');
}
function getRowHtml(id, innerHtml) {
return '<tr id="'+id+'">'+innerHtml+'</tr>';
}
function rowNode(row) {
return $("#"+row.domId);
}
function handleRowData(row) {
if (row.data && row.data.status == 'Disconnected') {
row.opacity = 0.5;
}
else {
delete row.opacity;
}
}
function handleRowNode(tr, data) {
if (data.titleText) {
var titleText = data.titleText;
window.setTimeout(function() { tr.attr('title', titleText )}, 0);
}
else {
tr.removeAttr('title');
}
}
function handleOtherUserInputs() {
// handle 'INPUT' elements for naming other unnamed users
$("#otheruserstable input.newinput").each(function() {
var input = $(this);
var tr = input.closest("tr");
if (tr.length > 0) {
var index = tr.parent().children().index(tr);
if (index >= 0) {
var userId = rowsPresent[index].data.id;
rowManagerMakeNameEditor($(this), userId);
}
}
}).removeClass('newinput');
}
// animationPower is 0 to skip animation, 1 for linear, 2 for quadratic, etc.
function insertRow(position, data, animationPower) {
position = Math.max(0, Math.min(rowsPresent.length, position));
animationPower = (animationPower === undefined ? 4 : animationPower);
var domId = nextRowId();
var row = {data: data, animationStep: ANIMATION_START, domId: domId,
animationPower: animationPower};
handleRowData(row);
rowsPresent.splice(position, 0, row);
var tr;
if (animationPower == 0) {
tr = $(getRowHtml(domId, getUserRowHtml(getAnimationHeight(0), data)));
row.animationStep = 0;
}
else {
rowsFadingIn.push(row);
tr = $(getRowHtml(domId, getEmptyRowHtml(getAnimationHeight(ANIMATION_START))));
}
handleRowNode(tr, data);
if (position == 0) {
$("table#otheruserstable").prepend(tr);
}
else {
rowNode(rowsPresent[position-1]).after(tr);
}
if (animationPower != 0) {
scheduleAnimation();
}
handleOtherUserInputs();
return row;
}
function updateRow(position, data) {
var row = rowsPresent[position];
if (row) {
row.data = data;
handleRowData(row);
if (row.animationStep == 0) {
// not currently animating
var tr = rowNode(row);
replaceUserRowContents(tr, getAnimationHeight(0), row.data).find(
"td").css('opacity', (row.opacity === undefined ? 1 : row.opacity));
handleRowNode(tr, data);
handleOtherUserInputs();
}
}
}
function removeRow(position, animationPower) {
animationPower = (animationPower === undefined ? 4 : animationPower);
var row = rowsPresent[position];
if (row) {
rowsPresent.splice(position, 1); // remove
if (animationPower == 0) {
rowNode(row).remove();
}
else {
row.animationStep = - row.animationStep; // use symmetry
row.animationPower = animationPower;
rowsFadingOut.push(row);
scheduleAnimation();
}
}
}
// newPosition is position after the row has been removed
function moveRow(oldPosition, newPosition, animationPower) {
animationPower = (animationPower === undefined ? 1 : animationPower); // linear is best
var row = rowsPresent[oldPosition];
if (row && oldPosition != newPosition) {
var rowData = row.data;
removeRow(oldPosition, animationPower);
insertRow(newPosition, rowData, animationPower);
}
}
function animateStep() {
// animation must be symmetrical
for(var i=rowsFadingIn.length-1;i>=0;i--) { // backwards to allow removal
var row = rowsFadingIn[i];
var step = ++row.animationStep;
var animHeight = getAnimationHeight(step, row.animationPower);
var node = rowNode(row);
var baseOpacity = (row.opacity === undefined ? 1 : row.opacity);
if (step <= -OPACITY_STEPS) {
node.find("td").height(animHeight);
}
else if (step == -OPACITY_STEPS+1) {
node.html(getUserRowHtml(animHeight, row.data)).find("td").css(
'opacity', baseOpacity*1/OPACITY_STEPS);
handleRowNode(node, row.data);
}
else if (step < 0) {
node.find("td").css('opacity', baseOpacity*(OPACITY_STEPS-(-step))/OPACITY_STEPS).height(animHeight);
}
else if (step == 0) {
// set HTML in case modified during animation
node.html(getUserRowHtml(animHeight, row.data)).find("td").css(
'opacity', baseOpacity*1).height(animHeight);
handleRowNode(node, row.data);
rowsFadingIn.splice(i, 1); // remove from set
}
}
for(var i=rowsFadingOut.length-1;i>=0;i--) { // backwards to allow removal
var row = rowsFadingOut[i];
var step = ++row.animationStep;
var node = rowNode(row);
var animHeight = getAnimationHeight(step, row.animationPower);
var baseOpacity = (row.opacity === undefined ? 1 : row.opacity);
if (step < OPACITY_STEPS) {
node.find("td").css('opacity', baseOpacity*(OPACITY_STEPS - step)/OPACITY_STEPS).height(animHeight);
}
else if (step == OPACITY_STEPS) {
node.html(getEmptyRowHtml(animHeight));
}
else if (step <= ANIMATION_END) {
node.find("td").height(animHeight);
}
else {
rowsFadingOut.splice(i, 1); // remove from set
node.remove();
}
}
handleOtherUserInputs();
return (rowsFadingIn.length > 0) || (rowsFadingOut.length > 0); // is more to do
}
var self = {
insertRow: insertRow,
removeRow: removeRow,
moveRow: moveRow,
updateRow: updateRow
};
return self;
}()); ////////// rowManager
var myUserInfo = {};
var otherUsersInfo = [];
var otherUsersData = [];
var colorPickerOpen = false;
function rowManagerMakeNameEditor(jnode, userId) {
setUpEditable(jnode, function() {
var existingIndex = findExistingIndex(userId);
if (existingIndex >= 0) {
return otherUsersInfo[existingIndex].name || '';
}
else {
return '';
}
}, function(newName) {
if (! newName) {
jnode.addClass("editempty");
jnode.val("unnamed");
}
else {
jnode.attr('disabled', 'disabled');
pad.suggestUserName(userId, newName);
}
});
}
function renderMyUserInfo() {
if (myUserInfo.name) {
$("#myusernameedit").removeClass("editempty").val(
myUserInfo.name);
}
else {
$("#myusernameedit").addClass("editempty").val(
"< enter your name >");
}
if (colorPickerOpen) {
$("#myswatchbox").addClass('myswatchboxunhoverable').removeClass(
'myswatchboxhoverable');
}
else {
$("#myswatchbox").addClass('myswatchboxhoverable').removeClass(
'myswatchboxunhoverable');
}
$("#myswatch").css('background', pad.getColorPalette()[myUserInfo.colorId]);
}
function findExistingIndex(userId) {
var existingIndex = -1;
for(var i=0;i<otherUsersInfo.length;i++) {
if (otherUsersInfo[i].userId == userId) {
existingIndex = i;
break;
}
}
return existingIndex;
}
function setUpEditable(jqueryNode, valueGetter, valueSetter) {
jqueryNode.bind('focus', function(evt) {
var oldValue = valueGetter();
if (jqueryNode.val() !== oldValue) {
jqueryNode.val(oldValue);
}
jqueryNode.addClass("editactive").removeClass("editempty");
});
jqueryNode.bind('blur', function(evt) {
var newValue = jqueryNode.removeClass("editactive").val();
valueSetter(newValue);
});
padutils.bindEnterAndEscape(jqueryNode, function onEnter() {
jqueryNode.blur();
}, function onEscape() {
jqueryNode.val(valueGetter()).blur();
});
jqueryNode.removeAttr('disabled').addClass('editable');
}
function showColorPicker() {
if (! colorPickerOpen) {
var palette = pad.getColorPalette();
for(var i=0;i<palette.length;i++) {
$("#mycolorpicker .n"+(i+1)+" .pickerswatch").css(
'background', palette[i]);
}
$("#mycolorpicker").css('display', 'block');
colorPickerOpen = true;
renderMyUserInfo();
}
// this part happens even if color picker is already open
$("#mycolorpicker .pickerswatchouter").removeClass('picked');
$("#mycolorpicker .pickerswatchouter:eq("+(myUserInfo.colorId||0)+")").
addClass('picked');
}
function getColorPickerSwatchIndex(jnode) {
return Number(jnode.get(0).className.match(/\bn([0-9]+)\b/)[1])-1;
}
function closeColorPicker(accept) {
if (accept) {
var newColorId = getColorPickerSwatchIndex($("#mycolorpicker .picked"));
if (newColorId >= 0) { // fails on NaN
myUserInfo.colorId = newColorId;
pad.notifyChangeColor(newColorId);
}
}
colorPickerOpen = false;
$("#mycolorpicker").css('display', 'none');
renderMyUserInfo();
}
function updateInviteNotice() {
if (otherUsersInfo.length == 0) {
$("#otheruserstable").hide();
$("#nootherusers").show();
}
else {
$("#nootherusers").hide();
$("#otheruserstable").show();
}
}
var knocksToIgnore = {};
var guestPromptFlashState = 0;
var guestPromptFlash = padutils.makeAnimationScheduler(
function () {
var prompts = $("#guestprompts .guestprompt");
if (prompts.length == 0) {
return false; // no more to do
}
guestPromptFlashState = 1 - guestPromptFlashState;
if (guestPromptFlashState) {
prompts.css('background', '#ffa');
}
else {
prompts.css('background', '#ffe');
}
return true;
}, 1000);
var self = {
init: function(myInitialUserInfo) {
self.setMyUserInfo(myInitialUserInfo);
$("#otheruserstable tr").remove();
if (pad.getUserIsGuest()) {
$("#myusernameedit").addClass('myusernameedithoverable');
setUpEditable($("#myusernameedit"),
function() {
return myUserInfo.name || '';
},
function(newValue) {
myUserInfo.name = newValue;
pad.notifyChangeName(newValue);
// wrap with setTimeout to do later because we get
// a double "blur" fire in IE...
window.setTimeout(function() {
renderMyUserInfo();
}, 0);
});
}
// color picker
$("#myswatchbox").click(showColorPicker);
$("#mycolorpicker .pickerswatchouter").click(function() {
$("#mycolorpicker .pickerswatchouter").removeClass('picked');
$(this).addClass('picked');
});
$("#mycolorpickersave").click(function() {
closeColorPicker(true);
});
$("#mycolorpickercancel").click(function() {
closeColorPicker(false);
});
//
},
setMyUserInfo: function(info) {
myUserInfo = $.extend({}, info);
renderMyUserInfo();
},
userJoinOrUpdate: function(info) {
if ((! info.userId) || (info.userId == myUserInfo.userId)) {
// not sure how this would happen
return;
}
var userData = {};
userData.color = pad.getColorPalette()[info.colorId];
userData.name = info.name;
userData.status = '';
userData.activity = '';
userData.id = info.userId;
// Firefox ignores \n in title text; Safari does a linebreak
userData.titleText = [info.userAgent||'', info.ip||''].join(' \n');
var existingIndex = findExistingIndex(info.userId);
var numUsersBesides = otherUsersInfo.length;
if (existingIndex >= 0) {
numUsersBesides--;
}
var newIndex = padutils.binarySearch(numUsersBesides, function(n) {
if (existingIndex >= 0 && n >= existingIndex) {
// pretend existingIndex isn't there
n++;
}
var infoN = otherUsersInfo[n];
var nameN = (infoN.name||'').toLowerCase();
var nameThis = (info.name||'').toLowerCase();
var idN = infoN.userId;
var idThis = info.userId;
return (nameN > nameThis) || (nameN == nameThis &&
idN > idThis);
});
if (existingIndex >= 0) {
// update
if (existingIndex == newIndex) {
otherUsersInfo[existingIndex] = info;
otherUsersData[existingIndex] = userData;
rowManager.updateRow(existingIndex, userData);
}
else {
otherUsersInfo.splice(existingIndex, 1);
otherUsersData.splice(existingIndex, 1);
otherUsersInfo.splice(newIndex, 0, info);
otherUsersData.splice(newIndex, 0, userData);
rowManager.updateRow(existingIndex, userData);
rowManager.moveRow(existingIndex, newIndex);
}
}
else {
otherUsersInfo.splice(newIndex, 0, info);
otherUsersData.splice(newIndex, 0, userData);
rowManager.insertRow(newIndex, userData);
}
updateInviteNotice();
},
userLeave: function(info) {
var existingIndex = findExistingIndex(info.userId);
if (existingIndex >= 0) {
var userData = otherUsersData[existingIndex];
userData.status = 'Disconnected';
rowManager.updateRow(existingIndex, userData);
if (userData.leaveTimer) {
window.clearTimeout(userData.leaveTimer);
}
// set up a timer that will only fire if no leaves,
// joins, or updates happen for this user in the
// next N seconds, to remove the user from the list.
var thisUserId = info.userId;
var thisLeaveTimer = window.setTimeout(function() {
var newExistingIndex = findExistingIndex(thisUserId);
if (newExistingIndex >= 0) {
var newUserData = otherUsersData[newExistingIndex];
if (newUserData.status == 'Disconnected' &&
newUserData.leaveTimer == thisLeaveTimer) {
otherUsersInfo.splice(newExistingIndex, 1);
otherUsersData.splice(newExistingIndex, 1);
rowManager.removeRow(newExistingIndex);
updateInviteNotice();
}
}
}, 8000); // how long to wait
userData.leaveTimer = thisLeaveTimer;
}
updateInviteNotice();
},
showGuestPrompt: function(userId, displayName) {
if (knocksToIgnore[userId]) {
return;
}
var encodedUserId = padutils.encodeUserId(userId);
var actionName = 'hide-guest-prompt-'+encodedUserId;
padutils.cancelActions(actionName);
var box = $("#guestprompt-"+encodedUserId);
if (box.length == 0) {
// make guest prompt box
box = $('<div id="guestprompt-'+encodedUserId+'" class="guestprompt"><div class="choices"><a href="javascript:void(paduserlist.answerGuestPrompt(\''+encodedUserId+'\',false))">Deny</a> <a href="javascript:void(paduserlist.answerGuestPrompt(\''+encodedUserId+'\',true))">Approve</a></div><div class="guestname"><strong>Guest:</strong> '+padutils.escapeHtml(displayName)+'</div></div>');
$("#guestprompts").append(box);
}
else {
// update display name
box.find(".guestname").html('<strong>Guest:</strong> '+padutils.escapeHtml(displayName));
}
var hideLater = padutils.getCancellableAction(actionName, function() {
self.removeGuestPrompt(userId);
});
window.setTimeout(hideLater, 15000); // time-out with no knock
guestPromptFlash.scheduleAnimation();
},
removeGuestPrompt: function(userId) {
var box = $("#guestprompt-"+padutils.encodeUserId(userId));
// remove ID now so a new knock by same user gets new, unfaded box
box.removeAttr('id').fadeOut("fast", function() {
box.remove();
});
knocksToIgnore[userId] = true;
window.setTimeout(function() {
delete knocksToIgnore[userId];
}, 5000);
},
answerGuestPrompt: function(encodedUserId, approve) {
var guestId = padutils.decodeUserId(encodedUserId);
var msg = {
type: 'guestanswer',
authId: pad.getUserId(),
guestId: guestId,
answer: (approve ? "approved" : "denied")
};
pad.sendClientMessage(msg);
self.removeGuestPrompt(guestId);
}
};
return self;
}());

363
static/js/pad_utils.js Normal file
View file

@ -0,0 +1,363 @@
/**
* 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 padutils = {
escapeHtml: function(x) {
return String(x).replace(/\</g, '&lt;').replace(/\>/g, '&gt;');
},
uniqueId: function() {
function encodeNum(n, width) {
// returns string that is exactly 'width' chars, padding with zeros
// and taking rightmost digits
return (Array(width+1).join('0') + Number(n).toString(35)).slice(-width);
}
return [pad.getClientIp(),
encodeNum(+new Date, 7),
encodeNum(Math.floor(Math.random()*1e9), 4)].join('.');
},
uaDisplay: function(ua) {
var m;
function clean(a) {
var maxlen = 16;
a = a.replace(/[^a-zA-Z0-9\.]/g, '');
if (a.length > maxlen) {
a = a.substr(0,maxlen);
}
return a;
}
function checkver(name) {
var m = ua.match(RegExp(name + '\\/([\\d\\.]+)'));
if (m && m.length > 1) {
return clean(name+m[1]);
}
return null;
}
// firefox
if (checkver('Firefox')) { return checkver('Firefox'); }
// misc browsers, including IE
m = ua.match(/compatible; ([^;]+);/);
if (m && m.length > 1) {
return clean(m[1]);
}
// iphone
if (ua.match(/\(iPhone;/)) {
return 'iPhone';
}
// chrome
if (checkver('Chrome')) { return checkver('Chrome'); }
// safari
m = ua.match(/Safari\/[\d\.]+/);
if (m) {
var v = '?';
m = ua.match(/Version\/([\d\.]+)/);
if (m && m.length > 1) {
v = m[1];
}
return clean('Safari'+v);
}
// everything else
var x = ua.split(' ')[0];
return clean(x);
},
// "func" is a function over 0..(numItems-1) that is monotonically
// "increasing" with index (false, then true). Finds the boundary
// between false and true, a number between 0 and numItems inclusive.
binarySearch: function (numItems, func) {
if (numItems < 1) return 0;
if (func(0)) return 0;
if (! func(numItems-1)) return numItems;
var low = 0; // func(low) is always false
var high = numItems-1; // func(high) is always true
while ((high - low) > 1) {
var x = Math.floor((low+high)/2); // x != low, x != high
if (func(x)) high = x;
else low = x;
}
return high;
},
// e.g. "Thu Jun 18 2009 13:09"
simpleDateTime: function(date) {
var d = new Date(+date); // accept either number or date
var dayOfWeek = (['Sun','Mon','Tue','Wed','Thu','Fri','Sat'])[d.getDay()];
var month = (['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'])[d.getMonth()];
var dayOfMonth = d.getDate();
var year = d.getFullYear();
var hourmin = d.getHours()+":"+("0"+d.getMinutes()).slice(-2);
return dayOfWeek+' '+month+' '+dayOfMonth+' '+year+' '+hourmin;
},
findURLs: function(text) {
// copied from ACE
var _REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/;
var _REGEX_URLCHAR = new RegExp('('+/[-:@a-zA-Z0-9_.,~%+\/?=&#;()$]/.source+'|'+_REGEX_WORDCHAR.source+')');
var _REGEX_URL = new RegExp(/(?:(?:https?|s?ftp|ftps|file|smb|afp|nfs|(x-)?man|gopher|txmt):\/\/|mailto:)/.source+_REGEX_URLCHAR.source+'*(?![:.,;])'+_REGEX_URLCHAR.source, 'g');
// returns null if no URLs, or [[startIndex1, url1], [startIndex2, url2], ...]
function _findURLs(text) {
_REGEX_URL.lastIndex = 0;
var urls = null;
var execResult;
while ((execResult = _REGEX_URL.exec(text))) {
urls = (urls || []);
var startIndex = execResult.index;
var url = execResult[0];
urls.push([startIndex, url]);
}
return urls;
}
return _findURLs(text);
},
escapeHtmlWithClickableLinks: function(text, target) {
var idx = 0;
var pieces = [];
var urls = padutils.findURLs(text);
function advanceTo(i) {
if (i > idx) {
pieces.push(padutils.escapeHtml(text.substring(idx, i)));
idx = i;
}
}
if (urls) {
for(var j=0;j<urls.length;j++) {
var startIndex = urls[j][0];
var href = urls[j][1];
advanceTo(startIndex);
pieces.push('<a ', (target?'target="'+target+'" ':''),
'href="', href.replace(/\"/g, '&quot;'), '">');
advanceTo(startIndex + href.length);
pieces.push('</a>');
}
}
advanceTo(text.length);
return pieces.join('');
},
bindEnterAndEscape: function(node, onEnter, onEscape) {
// Use keypress instead of keyup in bindEnterAndEscape
// Keyup event is fired on enter in IME (Input Method Editor), But
// keypress is not. So, I changed to use keypress instead of keyup.
// It is work on Windows (IE8, Chrome 6.0.472), CentOs (Firefox 3.0) and Mac OSX (Firefox 3.6.10, Chrome 6.0.472, Safari 5.0).
if (onEnter) {
node.keypress( function(evt) {
if (evt.which == 13) {
onEnter(evt);
}
});
}
if (onEscape) {
node.keydown( function(evt) {
if (evt.which == 27) {
onEscape(evt);
}
});
}
},
timediff: function(d) {
function format(n, word) {
n = Math.round(n);
return ('' + n + ' ' + word + (n != 1 ? 's' : '') + ' ago');
}
d = Math.max(0, (+(new Date) - (+d) - pad.clientTimeOffset) / 1000);
if (d < 60) { return format(d, 'second'); }
d /= 60;
if (d < 60) { return format(d, 'minute'); }
d /= 60;
if (d < 24) { return format(d, 'hour'); }
d /= 24;
return format(d, 'day');
},
makeAnimationScheduler: function(funcToAnimateOneStep, stepTime, stepsAtOnce) {
if (stepsAtOnce === undefined) {
stepsAtOnce = 1;
}
var animationTimer = null;
function scheduleAnimation() {
if (! animationTimer) {
animationTimer = window.setTimeout(function() {
animationTimer = null;
var n = stepsAtOnce;
var moreToDo = true;
while (moreToDo && n > 0) {
moreToDo = funcToAnimateOneStep();
n--;
}
if (moreToDo) {
// more to do
scheduleAnimation();
}
}, stepTime*stepsAtOnce);
}
}
return { scheduleAnimation: scheduleAnimation };
},
makeShowHideAnimator: function(funcToArriveAtState, initiallyShown, fps, totalMs) {
var animationState = (initiallyShown ? 0 : -2); // -2 hidden, -1 to 0 fade in, 0 to 1 fade out
var animationFrameDelay = 1000 / fps;
var animationStep = animationFrameDelay / totalMs;
var scheduleAnimation =
padutils.makeAnimationScheduler(animateOneStep, animationFrameDelay).scheduleAnimation;
function doShow() {
animationState = -1;
funcToArriveAtState(animationState);
scheduleAnimation();
}
function doQuickShow() { // start showing without losing any fade-in progress
if (animationState < -1) {
animationState = -1;
}
else if (animationState <= 0) {
animationState = animationState;
}
else {
animationState = Math.max(-1, Math.min(0, - animationState));
}
funcToArriveAtState(animationState);
scheduleAnimation();
}
function doHide() {
if (animationState >= -1 && animationState <= 0) {
animationState = 1e-6;
scheduleAnimation();
}
}
function animateOneStep() {
if (animationState < -1 || animationState == 0) {
return false;
}
else if (animationState < 0) {
// animate show
animationState += animationStep;
if (animationState >= 0) {
animationState = 0;
funcToArriveAtState(animationState);
return false;
}
else {
funcToArriveAtState(animationState);
return true;
}
}
else if (animationState > 0) {
// animate hide
animationState += animationStep;
if (animationState >= 1) {
animationState = 1;
funcToArriveAtState(animationState);
animationState = -2;
return false;
}
else {
funcToArriveAtState(animationState);
return true;
}
}
}
return {show: doShow, hide: doHide, quickShow: doQuickShow};
},
_nextActionId: 1,
uncanceledActions: {},
getCancellableAction: function(actionType, actionFunc) {
var o = padutils.uncanceledActions[actionType];
if (! o) {
o = {};
padutils.uncanceledActions[actionType] = o;
}
var actionId = (padutils._nextActionId++);
o[actionId] = true;
return function() {
var p = padutils.uncanceledActions[actionType];
if (p && p[actionId]) {
actionFunc();
}
};
},
cancelActions: function(actionType) {
var o = padutils.uncanceledActions[actionType];
if (o) {
// clear it
delete padutils.uncanceledActions[actionType];
}
},
makeFieldLabeledWhenEmpty: function(field, labelText) {
field = $(field);
function clear() {
field.addClass('editempty');
field.val(labelText);
}
field.focus(function() {
if (field.hasClass('editempty')) {
field.val('');
}
field.removeClass('editempty');
});
field.blur(function() {
if (! field.val()) {
clear();
}
});
return {clear:clear};
},
getCheckbox: function(node) {
return $(node).is(':checked');
},
setCheckbox: function(node, value) {
if (value) {
$(node).attr('checked', 'checked');
}
else {
$(node).removeAttr('checked');
}
},
bindCheckboxChange: function(node, func) {
$(node).bind("click change", func);
},
encodeUserId: function(userId) {
return userId.replace(/[^a-y0-9]/g, function(c) {
if (c == ".") return "-";
return 'z'+c.charCodeAt(0)+'z';
});
},
decodeUserId: function(encodedUserId) {
return encodedUserId.replace(/[a-y0-9]+|-|z.+?z/g, function(cc) {
if (cc == '-') return '.';
else if (cc.charAt(0) == 'z') {
return String.fromCharCode(Number(cc.slice(1,-1)));
}
else {
return cc;
}
});
}
};

22
static/js/plugins.js Normal file
View file

@ -0,0 +1,22 @@
plugins = {
callHook: function (hookName, args) {
var hook = clientVars.hooks[hookName];
if (hook === undefined)
return [];
var res = [];
for (var i = 0, N=hook.length; i < N; i++) {
var plugin = hook[i];
var pluginRes = eval(plugin.plugin)[plugin.original || hookName](args);
if (pluginRes != undefined && pluginRes != null)
res = res.concat(pluginRes);
}
return res;
},
callHookStr: function (hookName, args, sep, pre, post) {
if (sep == undefined) sep = '';
if (pre == undefined) pre = '';
if (post == undefined) post = '';
return plugins.callHook(hookName, args).map(function (x) { return pre + x + post}).join(sep || "");
}
};

347
static/js/skiplist.js Normal file
View file

@ -0,0 +1,347 @@
/**
* 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 newSkipList() {
var PROFILER = window.PROFILER;
if (!PROFILER) {
PROFILER = function() { return {start:noop, mark:noop, literal:noop, end:noop, cancel:noop}; };
}
function noop() {}
// if there are N elements in the skiplist, "start" is element -1 and "end" is element N
var start = {key:null, levels: 1, upPtrs:[null], downPtrs:[null], downSkips:[1], downSkipWidths:[0]};
var end = {key:null, levels: 1, upPtrs:[null], downPtrs:[null], downSkips:[null], downSkipWidths:[null]};
var numNodes = 0;
var totalWidth = 0;
var keyToNodeMap = {};
start.downPtrs[0] = end;
end.upPtrs[0] = start;
// a "point" object at location x allows modifications immediately after the first
// x elements of the skiplist, such as multiple inserts or deletes.
// After an insert or delete using point P, the point is still valid and points
// to the same index in the skiplist. Other operations with other points invalidate
// this point.
function _getPoint(targetLoc) {
var numLevels = start.levels;
var lvl = numLevels-1;
var i = -1, ws = 0;
var nodes = new Array(numLevels);
var idxs = new Array(numLevels);
var widthSkips = new Array(numLevels);
nodes[lvl] = start;
idxs[lvl] = -1;
widthSkips[lvl] = 0;
while (lvl >= 0) {
var n = nodes[lvl];
while (n.downPtrs[lvl] &&
(i + n.downSkips[lvl] < targetLoc)) {
i += n.downSkips[lvl];
ws += n.downSkipWidths[lvl];
n = n.downPtrs[lvl];
}
nodes[lvl] = n;
idxs[lvl] = i;
widthSkips[lvl] = ws;
lvl--;
if (lvl >= 0) {
nodes[lvl] = n;
}
}
return {nodes:nodes, idxs:idxs, loc:targetLoc, widthSkips:widthSkips, toString: function() {
return "getPoint("+targetLoc+")"; } };
}
function _getNodeAtOffset(targetOffset) {
var i = 0;
var n = start;
var lvl = start.levels-1;
while (lvl >= 0 && n.downPtrs[lvl]) {
while (n.downPtrs[lvl] && (i + n.downSkipWidths[lvl] <= targetOffset)) {
i += n.downSkipWidths[lvl];
n = n.downPtrs[lvl];
}
lvl--;
}
if (n === start) return (start.downPtrs[0] || null);
else if (n === end) return (targetOffset == totalWidth ? (end.upPtrs[0] || null) : null);
return n;
}
function _entryWidth(e) { return (e && e.width) || 0; }
function _insertKeyAtPoint(point, newKey, entry) {
var p = PROFILER("insertKey", false);
var newNode = {key:newKey, levels: 0, upPtrs:[], downPtrs:[], downSkips:[], downSkipWidths:[]};
p.mark("donealloc");
var pNodes = point.nodes;
var pIdxs = point.idxs;
var pLoc = point.loc;
var widthLoc = point.widthSkips[0] + point.nodes[0].downSkipWidths[0];
var newWidth = _entryWidth(entry);
p.mark("loop1");
while (newNode.levels == 0 || Math.random() < 0.01) {
var lvl = newNode.levels;
newNode.levels++;
if (lvl == pNodes.length) {
// assume we have just passed the end of point.nodes, and reached one level greater
// than the skiplist currently supports
pNodes[lvl] = start;
pIdxs[lvl] = -1;
start.levels++;
end.levels++;
start.downPtrs[lvl] = end;
end.upPtrs[lvl] = start;
start.downSkips[lvl] = numNodes+1;
start.downSkipWidths[lvl] = totalWidth;
point.widthSkips[lvl] = 0;
}
var me = newNode;
var up = pNodes[lvl];
var down = up.downPtrs[lvl];
var skip1 = pLoc - pIdxs[lvl];
var skip2 = up.downSkips[lvl] + 1 - skip1;
up.downSkips[lvl] = skip1;
up.downPtrs[lvl] = me;
me.downSkips[lvl] = skip2;
me.upPtrs[lvl] = up;
me.downPtrs[lvl] = down;
down.upPtrs[lvl] = me;
var widthSkip1 = widthLoc - point.widthSkips[lvl];
var widthSkip2 = up.downSkipWidths[lvl] + newWidth - widthSkip1;
up.downSkipWidths[lvl] = widthSkip1;
me.downSkipWidths[lvl] = widthSkip2;
}
p.mark("loop2");
p.literal(pNodes.length, "PNL");
for(var lvl=newNode.levels; lvl<pNodes.length; lvl++) {
var up = pNodes[lvl];
up.downSkips[lvl]++;
up.downSkipWidths[lvl] += newWidth;
}
p.mark("map");
keyToNodeMap['$KEY$'+newKey] = newNode;
numNodes++;
totalWidth += newWidth;
p.end();
}
function _getNodeAtPoint(point) {
return point.nodes[0].downPtrs[0];
}
function _incrementPoint(point) {
point.loc++;
for(var i=0;i<point.nodes.length;i++) {
if (point.idxs[i] + point.nodes[i].downSkips[i] < point.loc) {
point.idxs[i] += point.nodes[i].downSkips[i];
point.widthSkips[i] += point.nodes[i].downSkipWidths[i];
point.nodes[i] = point.nodes[i].downPtrs[i];
}
}
}
function _deleteKeyAtPoint(point) {
var elem = point.nodes[0].downPtrs[0];
var elemWidth = _entryWidth(elem.entry);
for(var i=0;i<point.nodes.length;i++) {
if (i < elem.levels) {
var up = elem.upPtrs[i];
var down = elem.downPtrs[i];
var totalSkip = up.downSkips[i] + elem.downSkips[i] - 1;
up.downPtrs[i] = down;
down.upPtrs[i] = up;
up.downSkips[i] = totalSkip;
var totalWidthSkip = up.downSkipWidths[i] + elem.downSkipWidths[i] - elemWidth;
up.downSkipWidths[i] = totalWidthSkip;
}
else {
var up = point.nodes[i];
var down = up.downPtrs[i];
up.downSkips[i]--;
up.downSkipWidths[i] -= elemWidth;
}
}
delete keyToNodeMap['$KEY$'+elem.key];
numNodes--;
totalWidth -= elemWidth;
}
function _propagateWidthChange(node) {
var oldWidth = node.downSkipWidths[0];
var newWidth = _entryWidth(node.entry);
var widthChange = newWidth - oldWidth;
var n = node;
var lvl = 0;
while (lvl < n.levels) {
n.downSkipWidths[lvl] += widthChange;
lvl++;
while (lvl >= n.levels && n.upPtrs[lvl-1]) {
n = n.upPtrs[lvl-1];
}
}
totalWidth += widthChange;
}
function _getNodeIndex(node, byWidth) {
var dist = (byWidth ? 0 : -1);
var n = node;
while (n !== start) {
var lvl = n.levels-1;
n = n.upPtrs[lvl];
if (byWidth) dist += n.downSkipWidths[lvl];
else dist += n.downSkips[lvl];
}
return dist;
}
/*function _debugToString() {
var array = [start];
while (array[array.length-1] !== end) {
array[array.length] = array[array.length-1].downPtrs[0];
}
function getIndex(node) {
if (!node) return null;
for(var i=0;i<array.length;i++) {
if (array[i] === node)
return i-1;
}
return false;
}
var processedArray = map(array, function(node) {
var x = {key:node.key, levels: node.levels, downSkips: node.downSkips,
upPtrs: map(node.upPtrs, getIndex), downPtrs: map(node.downPtrs, getIndex),
downSkipWidths: node.downSkipWidths};
return x;
});
return map(processedArray, function (x) { return x.toSource(); }).join("\n");
}*/
function _getNodeByKey(key) {
return keyToNodeMap['$KEY$'+key];
}
// Returns index of first entry such that entryFunc(entry) is truthy,
// or length() if no such entry. Assumes all falsy entries come before
// all truthy entries.
function _search(entryFunc) {
var low = start;
var lvl = start.levels-1;
var lowIndex = -1;
function f(node) {
if (node === start) return false;
else if (node === end) return true;
else return entryFunc(node.entry);
}
while (lvl >= 0) {
var nextLow = low.downPtrs[lvl];
while (!f(nextLow)) {
lowIndex += low.downSkips[lvl];
low = nextLow;
nextLow = low.downPtrs[lvl];
}
lvl--;
}
return lowIndex+1;
}
/*
The skip-list contains "entries", JavaScript objects that each must have a unique "key" property
that is a string.
*/
var self = {
length: function() { return numNodes; },
atIndex: function(i) {
if (i < 0) console.warn("atIndex("+i+")");
if (i >= numNodes) console.warn("atIndex("+i+">="+numNodes+")");
return _getNodeAtPoint(_getPoint(i)).entry;
},
// differs from Array.splice() in that new elements are in an array, not varargs
splice: function(start, deleteCount, newEntryArray) {
if (start < 0) console.warn("splice("+start+", ...)");
if (start + deleteCount > numNodes) {
console.warn("splice("+start+", "+deleteCount+", ...), N="+numNodes);
console.warn("%s %s %s", typeof start, typeof deleteCount, typeof numNodes);
console.trace();
}
if (! newEntryArray) newEntryArray = [];
var pt = _getPoint(start);
for(var i=0;i<deleteCount;i++) {
_deleteKeyAtPoint(pt);
}
for(var i=(newEntryArray.length-1);i>=0;i--) {
var entry = newEntryArray[i];
_insertKeyAtPoint(pt, entry.key, entry);
var node = _getNodeByKey(entry.key);
node.entry = entry;
}
},
next: function (entry) {
return _getNodeByKey(entry.key).downPtrs[0].entry || null;
},
prev: function (entry) {
return _getNodeByKey(entry.key).upPtrs[0].entry || null;
},
push: function(entry) {
self.splice(numNodes, 0, [entry]);
},
slice: function(start, end) {
// act like Array.slice()
if (start === undefined) start = 0;
else if (start < 0) start += numNodes;
if (end === undefined) end = numNodes;
else if (end < 0) end += numNodes;
if (start < 0) start = 0;
if (start > numNodes) start = numNodes;
if (end < 0) end = 0;
if (end > numNodes) end = numNodes;
dmesg(String([start,end,numNodes]));
if (end <= start) return [];
var n = self.atIndex(start);
var array = [n];
for(var i=1;i<(end-start);i++) {
n = self.next(n);
array.push(n);
}
return array;
},
atKey: function(key) { return _getNodeByKey(key).entry; },
indexOfKey: function(key) { return _getNodeIndex(_getNodeByKey(key)); },
indexOfEntry: function (entry) { return self.indexOfKey(entry.key); },
containsKey: function(key) { return !!(_getNodeByKey(key)); },
// gets the last entry starting at or before the offset
atOffset: function(offset) { return _getNodeAtOffset(offset).entry; },
keyAtOffset: function(offset) { return self.atOffset(offset).key; },
offsetOfKey: function(key) { return _getNodeIndex(_getNodeByKey(key), true); },
offsetOfEntry: function(entry) { return self.offsetOfKey(entry.key); },
setEntryWidth: function(entry, width) { entry.width = width; _propagateWidthChange(_getNodeByKey(entry.key)); },
totalWidth: function() { return totalWidth; },
offsetOfIndex: function(i) {
if (i < 0) return 0;
if (i >= numNodes) return totalWidth;
return self.offsetOfEntry(self.atIndex(i));
},
indexOfOffset: function(offset) {
if (offset <= 0) return 0;
if (offset >= totalWidth) return numNodes;
return self.indexOfEntry(self.atOffset(offset));
},
search: function(entryFunc) {
return _search(entryFunc);
},
//debugToString: _debugToString,
debugGetPoint: _getPoint,
debugDepth: function() { return start.levels; }
}
return self;
}

25
static/js/undo-xpopup.js Normal file
View file

@ -0,0 +1,25 @@
/**
* 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.
*/
if (window._orig_windowOpen) {
window.open = _orig_windowOpen;
}
if (window._orig_windowSetTimeout) {
window.setTimeout = _orig_windowSetTimeout;
}
if (window._orig_windowSetInterval) {
window.setInterval = _orig_windowSetInterval;
}

258
static/js/undomodule.js Normal file
View file

@ -0,0 +1,258 @@
/**
* 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.
*/
undoModule = (function() {
var stack = (function() {
var stackElements = [];
// two types of stackElements:
// 1) { elementType: UNDOABLE_EVENT, eventType: "anything", [backset: <changeset>,]
// [selStart: <char number>, selEnd: <char number>, selFocusAtStart: <boolean>] }
// 2) { elementType: EXTERNAL_CHANGE, changeset: <changeset> }
// invariant: no two consecutive EXTERNAL_CHANGEs
var numUndoableEvents = 0;
var UNDOABLE_EVENT = "undoableEvent";
var EXTERNAL_CHANGE = "externalChange";
function clearStack() {
stackElements.length = 0;
stackElements.push({ elementType: UNDOABLE_EVENT, eventType: "bottom" });
numUndoableEvents = 1;
}
clearStack();
function pushEvent(event) {
var e = extend({}, event);
e.elementType = UNDOABLE_EVENT;
stackElements.push(e);
numUndoableEvents++;
//dmesg("pushEvent backset: "+event.backset);
}
function pushExternalChange(cs) {
var idx = stackElements.length-1;
if (stackElements[idx].elementType == EXTERNAL_CHANGE) {
stackElements[idx].changeset = Changeset.compose(stackElements[idx].changeset, cs, getAPool());
}
else {
stackElements.push({elementType: EXTERNAL_CHANGE, changeset: cs});
}
}
function _exposeEvent(nthFromTop) {
// precond: 0 <= nthFromTop < numUndoableEvents
var targetIndex = stackElements.length - 1 - nthFromTop;
var idx = stackElements.length - 1;
while (idx > targetIndex || stackElements[idx].elementType == EXTERNAL_CHANGE) {
if (stackElements[idx].elementType == EXTERNAL_CHANGE) {
var ex = stackElements[idx];
var un = stackElements[idx-1];
if (un.backset) {
var excs = ex.changeset;
var unbs = un.backset;
un.backset = Changeset.follow(excs, un.backset, false, getAPool());
ex.changeset = Changeset.follow(unbs, ex.changeset, true, getAPool());
if ((typeof un.selStart) == "number") {
var newSel = Changeset.characterRangeFollow(excs, un.selStart, un.selEnd);
un.selStart = newSel[0];
un.selEnd = newSel[1];
if (un.selStart == un.selEnd) {
un.selFocusAtStart = false;
}
}
}
stackElements[idx-1] = ex;
stackElements[idx] = un;
if (idx >= 2 && stackElements[idx-2].elementType == EXTERNAL_CHANGE) {
ex.changeset = Changeset.compose(stackElements[idx-2].changeset,
ex.changeset, getAPool());
stackElements.splice(idx-2, 1);
idx--;
}
}
else {
idx--;
}
}
}
function getNthFromTop(n) {
// precond: 0 <= n < numEvents()
_exposeEvent(n);
return stackElements[stackElements.length - 1 - n];
}
function numEvents() {
return numUndoableEvents;
}
function popEvent() {
// precond: numEvents() > 0
_exposeEvent(0);
numUndoableEvents--;
return stackElements.pop();
}
return {numEvents:numEvents, popEvent:popEvent, pushEvent: pushEvent,
pushExternalChange: pushExternalChange, clearStack: clearStack,
getNthFromTop:getNthFromTop};
})();
// invariant: stack always has at least one undoable event
var undoPtr = 0; // zero-index from top of stack, 0 == top
function clearHistory() {
stack.clearStack();
undoPtr = 0;
}
function _charOccurrences(str, c) {
var i = 0;
var count = 0;
while (i >= 0 && i < str.length) {
i = str.indexOf(c, i);
if (i >= 0) {
count++;
i++;
}
}
return count;
}
function _opcodeOccurrences(cs, opcode) {
return _charOccurrences(Changeset.unpack(cs).ops, opcode);
}
function _mergeChangesets(cs1, cs2) {
if (! cs1) return cs2;
if (! cs2) return cs1;
// Rough heuristic for whether changesets should be considered one action:
// each does exactly one insertion, no dels, and the composition does also; or
// each does exactly one deletion, no ins, and the composition does also.
// A little weird in that it won't merge "make bold" with "insert char"
// but will merge "make bold and insert char" with "insert char",
// though that isn't expected to come up.
var plusCount1 = _opcodeOccurrences(cs1, '+');
var plusCount2 = _opcodeOccurrences(cs2, '+');
var minusCount1 = _opcodeOccurrences(cs1, '-');
var minusCount2 = _opcodeOccurrences(cs2, '-');
if (plusCount1 == 1 && plusCount2 == 1 && minusCount1 == 0 && minusCount2 == 0) {
var merge = Changeset.compose(cs1, cs2, getAPool());
var plusCount3 = _opcodeOccurrences(merge, '+');
var minusCount3 = _opcodeOccurrences(merge, '-');
if (plusCount3 == 1 && minusCount3 == 0) {
return merge;
}
}
else if (plusCount1 == 0 && plusCount2 == 0 && minusCount1 == 1 && minusCount2 == 1) {
var merge = Changeset.compose(cs1, cs2, getAPool());
var plusCount3 = _opcodeOccurrences(merge, '+');
var minusCount3 = _opcodeOccurrences(merge, '-');
if (plusCount3 == 0 && minusCount3 == 1) {
return merge;
}
}
return null;
}
function reportEvent(event) {
var topEvent = stack.getNthFromTop(0);
function applySelectionToTop() {
if ((typeof event.selStart) == "number") {
topEvent.selStart = event.selStart;
topEvent.selEnd = event.selEnd;
topEvent.selFocusAtStart = event.selFocusAtStart;
}
}
if ((! event.backset) || Changeset.isIdentity(event.backset)) {
applySelectionToTop();
}
else {
var merged = false;
if (topEvent.eventType == event.eventType) {
var merge = _mergeChangesets(event.backset, topEvent.backset);
if (merge) {
topEvent.backset = merge;
//dmesg("reportEvent merge: "+merge);
applySelectionToTop();
merged = true;
}
}
if (! merged) {
stack.pushEvent(event);
}
undoPtr = 0;
}
}
function reportExternalChange(changeset) {
if (changeset && ! Changeset.isIdentity(changeset)) {
stack.pushExternalChange(changeset);
}
}
function _getSelectionInfo(event) {
if ((typeof event.selStart) != "number") {
return null;
}
else {
return {selStart: event.selStart, selEnd: event.selEnd,
selFocusAtStart: event.selFocusAtStart};
}
}
// For "undo" and "redo", the change event must be returned
// by eventFunc and NOT reported through the normal mechanism.
// "eventFunc" should take a changeset and an optional selection info object,
// or can be called with no arguments to mean that no undo is possible.
// "eventFunc" will be called exactly once.
function performUndo(eventFunc) {
if (undoPtr < stack.numEvents()-1) {
var backsetEvent = stack.getNthFromTop(undoPtr);
var selectionEvent = stack.getNthFromTop(undoPtr+1);
var undoEvent = eventFunc(backsetEvent.backset, _getSelectionInfo(selectionEvent));
stack.pushEvent(undoEvent);
undoPtr += 2;
}
else eventFunc();
}
function performRedo(eventFunc) {
if (undoPtr >= 2) {
var backsetEvent = stack.getNthFromTop(0);
var selectionEvent = stack.getNthFromTop(1);
eventFunc(backsetEvent.backset, _getSelectionInfo(selectionEvent));
stack.popEvent();
undoPtr -= 2;
}
else eventFunc();
}
function getAPool() {
return undoModule.apool;
}
return {clearHistory:clearHistory, reportEvent:reportEvent, reportExternalChange:reportExternalChange,
performUndo:performUndo, performRedo:performRedo, enabled: true,
apool: null}; // apool is filled in by caller
})();

287
static/js/virtual_lines.js Normal file
View file

@ -0,0 +1,287 @@
/**
* 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 makeVirtualLineView(lineNode) {
// how much to jump forward or backward at once in a charSeeker before
// constructing a DOM node and checking the coordinates (which takes a
// significant fraction of a millisecond). From the
// coordinates and the approximate line height we can estimate how
// many lines we have moved. We risk being off if the number of lines
// we move is on the order of the line height in pixels. Fortunately,
// when the user boosts the font-size they increase both.
var maxCharIncrement = 20;
var seekerAtEnd = null;
function getNumChars() {
return lineNode.textContent.length;
}
function getNumVirtualLines() {
if (! seekerAtEnd) {
var seeker = makeCharSeeker();
seeker.forwardByWhile(maxCharIncrement);
seekerAtEnd = seeker;
}
return seekerAtEnd.getVirtualLine() + 1;
}
function getVLineAndOffsetForChar(lineChar) {
var seeker = makeCharSeeker();
seeker.forwardByWhile(maxCharIncrement, null, lineChar);
var theLine = seeker.getVirtualLine();
seeker.backwardByWhile(8, function() { return seeker.getVirtualLine() == theLine; });
seeker.forwardByWhile(1, function() { return seeker.getVirtualLine() != theLine; });
var lineStartChar = seeker.getOffset();
return {vline:theLine, offset:(lineChar - lineStartChar)};
}
function getCharForVLineAndOffset(vline, offset) {
// returns revised vline and offset as well as absolute char index within line.
// if offset is beyond end of line, for example, will give new offset at end of line.
var seeker = makeCharSeeker();
// go to start of line
seeker.binarySearch(function() {
return seeker.getVirtualLine() >= vline;
});
var lineStart = seeker.getOffset();
var theLine = seeker.getVirtualLine();
// go to offset, overshooting the virtual line only if offset is too large for it
seeker.forwardByWhile(maxCharIncrement, null, lineStart+offset);
// get back into line
seeker.backwardByWhile(1, function() { return seeker.getVirtualLine() != theLine; }, lineStart);
var lineChar = seeker.getOffset();
var theOffset = lineChar - lineStart;
// handle case of last virtual line; should be able to be at end of it
if (theOffset < offset && theLine == (getNumVirtualLines()-1)) {
var lineLen = getNumChars();
theOffset += lineLen-lineChar;
lineChar = lineLen;
}
return { vline:theLine, offset:theOffset, lineChar:lineChar };
}
return {getNumVirtualLines:getNumVirtualLines, getVLineAndOffsetForChar:getVLineAndOffsetForChar,
getCharForVLineAndOffset:getCharForVLineAndOffset,
makeCharSeeker: function() { return makeCharSeeker(); } };
function deepFirstChildTextNode(nd) {
nd = nd.firstChild;
while (nd && nd.firstChild) nd = nd.firstChild;
if (nd.data) return nd;
return null;
}
function makeCharSeeker(/*lineNode*/) {
function charCoords(tnode, i) {
var container = tnode.parentNode;
// treat space specially; a space at the end of a virtual line
// will have weird coordinates
var isSpace = (tnode.nodeValue.charAt(i) === " ");
if (isSpace) {
if (i == 0) {
if (container.previousSibling && deepFirstChildTextNode(container.previousSibling)) {
tnode = deepFirstChildTextNode(container.previousSibling);
i = tnode.length-1;
container = tnode.parentNode;
}
else {
return {top:container.offsetTop, left:container.offsetLeft};
}
}
else {
i--; // use previous char
}
}
var charWrapper = document.createElement("SPAN");
// wrap the character
var tnodeText = tnode.nodeValue;
var frag = document.createDocumentFragment();
frag.appendChild(document.createTextNode(tnodeText.substring(0, i)));
charWrapper.appendChild(document.createTextNode(tnodeText.substr(i, 1)));
frag.appendChild(charWrapper);
frag.appendChild(document.createTextNode(tnodeText.substring(i+1)));
container.replaceChild(frag, tnode);
var result = {top:charWrapper.offsetTop,
left:charWrapper.offsetLeft + (isSpace ? charWrapper.offsetWidth : 0),
height:charWrapper.offsetHeight};
while (container.firstChild) container.removeChild(container.firstChild);
container.appendChild(tnode);
return result;
}
var lineText = lineNode.textContent;
var lineLength = lineText.length;
var curNode = null;
var curChar = 0;
var curCharWithinNode = 0
var curTop;
var curLeft;
var approxLineHeight;
var whichLine = 0;
function nextNode() {
var n = curNode;
if (! n) n = lineNode.firstChild;
else n = n.nextSibling;
while (n && ! deepFirstChildTextNode(n)) {
n = n.nextSibling;
}
return n;
}
function prevNode() {
var n = curNode;
if (! n) n = lineNode.lastChild;
else n = n.previousSibling;
while (n && ! deepFirstChildTextNode(n)) {
n = n.previousSibling;
}
return n;
}
var seeker;
if (lineLength > 0) {
curNode = nextNode();
var firstCharData = charCoords(deepFirstChildTextNode(curNode), 0);
approxLineHeight = firstCharData.height;
curTop = firstCharData.top;
curLeft = firstCharData.left;
function updateCharData(tnode, i) {
var coords = charCoords(tnode, i);
whichLine += Math.round((coords.top - curTop) / approxLineHeight);
curTop = coords.top;
curLeft = coords.left;
}
seeker = {
forward: function(numChars) {
var oldChar = curChar;
var newChar = curChar + numChars;
if (newChar > (lineLength-1))
newChar = lineLength-1;
while (curChar < newChar) {
var curNodeLength = deepFirstChildTextNode(curNode).length;
var toGo = curNodeLength - curCharWithinNode;
if (curChar + toGo > newChar || ! nextNode()) {
// going to next node would be too far
var n = newChar - curChar;
if (n >= toGo) n = toGo-1;
curChar += n;
curCharWithinNode += n;
break;
}
else {
// go to next node
curChar += toGo;
curCharWithinNode = 0;
curNode = nextNode();
}
}
updateCharData(deepFirstChildTextNode(curNode), curCharWithinNode);
return curChar - oldChar;
},
backward: function(numChars) {
var oldChar = curChar;
var newChar = curChar - numChars;
if (newChar < 0) newChar = 0;
while (curChar > newChar) {
if (curChar - curCharWithinNode <= newChar || !prevNode()) {
// going to prev node would be too far
var n = curChar - newChar;
if (n > curCharWithinNode) n = curCharWithinNode;
curChar -= n;
curCharWithinNode -= n;
break;
}
else {
// go to prev node
curChar -= curCharWithinNode+1;
curNode = prevNode();
curCharWithinNode = deepFirstChildTextNode(curNode).length-1;
}
}
updateCharData(deepFirstChildTextNode(curNode), curCharWithinNode);
return oldChar - curChar;
},
getVirtualLine: function() { return whichLine; },
getLeftCoord: function() { return curLeft; }
};
}
else {
curLeft = lineNode.offsetLeft;
seeker = { forward: function(numChars) { return 0; },
backward: function(numChars) { return 0; },
getVirtualLine: function() { return 0; },
getLeftCoord: function() { return curLeft; }
};
}
seeker.getOffset = function() { return curChar; };
seeker.getLineLength = function() { return lineLength; };
seeker.toString = function() {
return "seeker[curChar: "+curChar+"("+lineText.charAt(curChar)+"), left: "+seeker.getLeftCoord()+", vline: "+seeker.getVirtualLine()+"]";
};
function moveByWhile(isBackward, amount, optCondFunc, optCharLimit) {
var charsMovedLast = null;
var hasCondFunc = ((typeof optCondFunc) == "function");
var condFunc = optCondFunc;
var hasCharLimit = ((typeof optCharLimit) == "number");
var charLimit = optCharLimit;
while (charsMovedLast !== 0 && ((! hasCondFunc) || condFunc())) {
var toMove = amount;
if (hasCharLimit) {
var untilLimit = (isBackward ? curChar - charLimit : charLimit - curChar);
if (untilLimit < toMove) toMove = untilLimit;
}
if (toMove < 0) break;
charsMovedLast = (isBackward ? seeker.backward(toMove) : seeker.forward(toMove));
}
}
seeker.forwardByWhile = function(amount, optCondFunc, optCharLimit) {
moveByWhile(false, amount, optCondFunc, optCharLimit);
}
seeker.backwardByWhile = function(amount, optCondFunc, optCharLimit) {
moveByWhile(true, amount, optCondFunc, optCharLimit);
}
seeker.binarySearch = function(condFunc) {
// returns index of boundary between false chars and true chars;
// positions seeker at first true char, or else last char
var trueFunc = condFunc;
var falseFunc = function() { return ! condFunc(); };
seeker.forwardByWhile(20, falseFunc);
seeker.backwardByWhile(20, trueFunc);
seeker.forwardByWhile(10, falseFunc);
seeker.backwardByWhile(5, trueFunc);
seeker.forwardByWhile(1, falseFunc);
return seeker.getOffset() + (condFunc() ? 0 : 1);
}
return seeker;
}
}