mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-04-24 01:16:15 -04:00
first-commit
This commit is contained in:
commit
325c322a27
207 changed files with 35989 additions and 0 deletions
253
static/js/ace.js
Normal file
253
static/js/ace.js
Normal 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"> </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
30
static/js/ace.js.old
Normal file
File diff suppressed because one or more lines are too long
252
static/js/ace.js.old.firsttry
Normal file
252
static/js/ace.js.old.firsttry
Normal 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"> </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
115
static/js/ace2_common.js
Normal 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, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/\r?\n/g, '\\n');
|
||||
}
|
4588
static/js/ace2_inner.js
Normal file
4588
static/js/ace2_inner.js
Normal file
File diff suppressed because it is too large
Load diff
170
static/js/changesettracker.js
Normal file
170
static/js/changesettracker.js
Normal 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
666
static/js/collab_client.js
Normal 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
92
static/js/colorutils.js
Normal 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])];
|
||||
}
|
520
static/js/contentcollector.js
Normal file
520
static/js/contentcollector.js
Normal 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) == " "
|
||||
&& ! 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
88
static/js/cssmanager.js
Normal 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
232
static/js/domline.js
Normal 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, '"')+'">';
|
||||
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 += ' ';
|
||||
}
|
||||
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 = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
}
|
||||
return s.replace(re, function(c) { return re.MAP[c]; });
|
||||
};
|
||||
|
||||
domline.processSpaces = function(s, doesWrap) {
|
||||
if (s.indexOf("<") < 0 && ! doesWrap) {
|
||||
// short-cut
|
||||
return s.replace(/ /g, ' ');
|
||||
}
|
||||
var parts = [];
|
||||
s.replace(/<[^>]*>?| |[^ <]+/g, function(m) { parts.push(m); });
|
||||
if (doesWrap) {
|
||||
var endOfLine = true;
|
||||
var beforeSpace = false;
|
||||
// last space in a run is normal, others are nbsp,
|
||||
// end of line is nbsp
|
||||
for(var i=parts.length-1;i>=0;i--) {
|
||||
var p = parts[i];
|
||||
if (p == " ") {
|
||||
if (endOfLine || beforeSpace)
|
||||
parts[i] = ' ';
|
||||
endOfLine = false;
|
||||
beforeSpace = true;
|
||||
}
|
||||
else if (p.charAt(0) != "<") {
|
||||
endOfLine = false;
|
||||
beforeSpace = false;
|
||||
}
|
||||
}
|
||||
// beginning of line is nbsp
|
||||
for(var i=0;i<parts.length;i++) {
|
||||
var p = parts[i];
|
||||
if (p == " ") {
|
||||
parts[i] = ' ';
|
||||
break;
|
||||
}
|
||||
else if (p.charAt(0) != "<") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
for(var i=0;i<parts.length;i++) {
|
||||
var p = parts[i];
|
||||
if (p == " ") {
|
||||
parts[i] = ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
return parts.join('');
|
||||
};
|
151
static/js/draggable.js
Normal file
151
static/js/draggable.js
Normal 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
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
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
480
static/js/json2.js
Normal 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 ' '),
|
||||
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
498
static/js/json2.js.old
Normal 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 ' '),
|
||||
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');
|
||||
};
|
||||
}
|
||||
})();
|
290
static/js/linestylefilter.js
Normal file
290
static/js/linestylefilter.js
Normal 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
567
static/js/pad2.js
Normal 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
295
static/js/pad_chat.js
Normal 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;
|
||||
}());
|
53
static/js/pad_connectionstatus.js
Normal file
53
static/js/pad_connectionstatus.js
Normal 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
101
static/js/pad_cookie.js
Normal 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
347
static/js/pad_docbar.js
Normal 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, '•'));
|
||||
}
|
||||
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
122
static/js/pad_editbar.js
Normal 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
124
static/js/pad_editor.js
Normal 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
187
static/js/pad_impexp.js
Normal 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
306
static/js/pad_modals.js
Normal 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
408
static/js/pad_savedrevs.js
Normal 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
605
static/js/pad_userlist.js
Normal 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+'"> </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
363
static/js/pad_utils.js
Normal 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, '<').replace(/\>/g, '>');
|
||||
},
|
||||
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, '"'), '">');
|
||||
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
22
static/js/plugins.js
Normal 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
347
static/js/skiplist.js
Normal 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
25
static/js/undo-xpopup.js
Normal 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
258
static/js/undomodule.js
Normal 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
287
static/js/virtual_lines.js
Normal 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;
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue