Merge pull request #2590 from ether/release/1.5.3

Release/1.5.3
This commit is contained in:
John McLear 2015-04-11 00:04:45 +01:00
commit b95395a130
41 changed files with 1015 additions and 199 deletions

View File

@ -1,3 +1,14 @@
# 1.5.3
* NEW: Accessibility support for Screen readers, includes new fonts and keyboard shortcuts
* NEW: API endpoint for Append Chat Message and Chat Backend Tests
* NEW: Error messages displayed on load are included in Default Pad Text (can be supressed)
* NEW: Content Collector can handle key values
* NEW: getAttributesOnPosition Method
* FIX: Firefox keeps attributes (bold etc) on cut/copy -> paste
* Fix: showControls=false now works
* Fix: Cut and Paste works...
* SECURITY: Don't allow read files on directory traversal
# 1.5.2
* NEW: Support for node version 0.12.x
* NEW: API endpoint saveRevision, getSavedRevisionCount and listSavedRevisions

View File

@ -46,6 +46,12 @@ Now, run `start.bat` and open <http://localhost:9001> in your browser.
Update to the latest version with `git pull origin`, then run `bin\installOnWindows.bat`, again.
If cloning to a subdirectory within another project, you may need to do the following:
1. Start the server manually (e.g. `node/node_modules/ep_etherpad-lite/node/server.js]`)
2. Edit the db `filename` in `settings.json` to the relative directory with the file (e.g. `application/lib/etherpad-lite/var/dirty.db`)
3. Add auto-generated files to the main project `.gitignore`
[Next steps](#next-steps).
## GNU/Linux and other UNIX-like systems

View File

@ -103,7 +103,7 @@ if [ $DOWNLOAD_JQUERY = "true" ]; then
fi
#Remove all minified data to force node creating it new
echo "Clear minfified cache..."
echo "Clearing minified cache..."
rm -f var/minified*
echo "Ensure custom css/js files are created..."

View File

@ -55,7 +55,7 @@ do
TIME_SINCE_LAST_SEND=$(($TIME_NOW - $LAST_EMAIL_SEND))
if [ $TIME_SINCE_LAST_SEND -gt $TIME_BETWEEN_EMAILS ]; then
printf "Server was restared at: $(date)\nThe last 50 lines of the log before the error happens:\n $(tail -n 50 ${LOG})" | mail -s "Pad Server was restarted" $EMAIL_ADDRESS
printf "Server was restarted at: $(date)\nThe last 50 lines of the log before the error happens:\n $(tail -n 50 ${LOG})" | mail -s "Pad Server was restarted" $EMAIL_ADDRESS
LAST_EMAIL_SEND=$TIME_NOW
fi

View File

@ -203,6 +203,13 @@ Things in context:
This hook is called before the content of a node is collected by the usual methods. The cc object can be used to do a bunch of things that modify the content of the pad. See, for example, the heading1 plugin for etherpad original.
E.g. if you need to apply an attribute to newly inserted characters,
call cc.doAttrib(state, "attributeName") which results in an attribute attributeName=true.
If you want to specify also a value, call cc.doAttrib(state, "attributeName:value")
which results in an attribute attributeName=value.
## collectContentImage
Called from: src/static/js/contentcollector.js

View File

@ -54,6 +54,9 @@
//the default text of a pad
"defaultPadText" : "Welcome to Etherpad!\n\nThis pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!\n\nGet involved with Etherpad at http:\/\/etherpad.org\n",
/* Shoud we suppress errors from being visible in the default Pad Text? */
"suppressErrorsInPadText" : false,
/* Users must have a session to access pads. This effectively allows only group pads to be accessed. */
"requireSession" : false,

View File

@ -38,7 +38,24 @@
"pad.settings.rtlcheck": "Read content from right to left?",
"pad.settings.fontType": "Font type:",
"pad.settings.fontType.normal": "Normal",
"pad.settings.fontType.opendyslexic": "Open Dyslexic",
"pad.settings.fontType.monospaced": "Monospace",
"pad.settings.fontType.comicsans": "Comic Sans",
"pad.settings.fontType.couriernew": "Courier New",
"pad.settings.fontType.georgia": "Georgia",
"pad.settings.fontType.impact": "Impact",
"pad.settings.fontType.lucida": "Lucida",
"pad.settings.fontType.lucidasans": "Lucida Sans",
"pad.settings.fontType.palatino": "Palatino",
"pad.settings.fontType.tahoma": "Tahoma",
"pad.settings.fontType.timesnewroman": "Times New Roman",
"pad.settings.fontType.trebuchet": "Trebuchet",
"pad.settings.fontType.verdana": "Verdana",
"pad.settings.fontType.symbol": "Symbol",
"pad.settings.fontType.webdings": "Webdings",
"pad.settings.fontType.wingdings": "Wingdings",
"pad.settings.fontType.sansserif": "Sans Serif",
"pad.settings.fontType.serif": "Serif",
"pad.settings.globalView": "Global View",
"pad.settings.language": "Language:",
@ -105,6 +122,10 @@
"timeslider.version": "Version {{version}}",
"timeslider.saved": "Saved {{month}} {{day}}, {{year}}",
"timeslider.playPause": "Playback / Pause Pad Contents",
"timeslider.backRevision":"Go back a revision in this Pad",
"timeslider.forwardRevision":"Go forward a revision in this Pad",
"timeslider.dateformat": "{{month}}/{{day}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}",
"timeslider.month.january": "January",
"timeslider.month.february": "February",

View File

@ -432,8 +432,8 @@ getChatHistory(padId, start, end), returns a part of or the whole chat-history o
Example returns:
{"code":0,"message":"ok","data":{"messages":[{"text":"foo","userId":"a.foo","time":1359199533759,"userName":"test"},
{"text":"bar","userId":"a.foo","time":1359199534622,"userName":"test"}]}}
{"code":0,"message":"ok","data":{"messages":[{"text":"foo","authorID":"a.foo","time":1359199533759,"userName":"test"},
{"text":"bar","authorID":"a.foo","time":1359199534622,"userName":"test"}]}}
{code: 1, message:"start is higher or equal to the current chatHead", data: null}
@ -494,6 +494,33 @@ exports.getChatHistory = function(padID, start, end, callback)
});
}
/**
appendChatMessage(padID, text, authorID, time), creates a chat message for the pad id, time is a timestamp
Example returns:
{code: 0, message:"ok", data: null
{code: 1, message:"padID does not exist", data: null}
*/
exports.appendChatMessage = function(padID, text, authorID, time, callback)
{
//text is required
if(typeof text != "string")
{
callback(new customError("text is no string","apierror"));
return;
}
//get the pad
getPadSafe(padID, true, function(err, pad)
{
if(ERR(err, callback)) return;
pad.appendChatMessage(text, authorID, parseInt(time));
callback();
});
}
/*****************/
/**PAD FUNCTIONS */
/*****************/

View File

@ -394,10 +394,60 @@ var version =
, "getChatHead" : ["padID"]
, "restoreRevision" : ["padID", "rev"]
}
, "1.2.12":
{ "createGroup" : []
, "createGroupIfNotExistsFor" : ["groupMapper"]
, "deleteGroup" : ["groupID"]
, "listPads" : ["groupID"]
, "listAllPads" : []
, "createDiffHTML" : ["padID", "startRev", "endRev"]
, "createPad" : ["padID", "text"]
, "createGroupPad" : ["groupID", "padName", "text"]
, "createAuthor" : ["name"]
, "createAuthorIfNotExistsFor": ["authorMapper" , "name"]
, "listPadsOfAuthor" : ["authorID"]
, "createSession" : ["groupID", "authorID", "validUntil"]
, "deleteSession" : ["sessionID"]
, "getSessionInfo" : ["sessionID"]
, "listSessionsOfGroup" : ["groupID"]
, "listSessionsOfAuthor" : ["authorID"]
, "getText" : ["padID", "rev"]
, "setText" : ["padID", "text"]
, "getHTML" : ["padID", "rev"]
, "setHTML" : ["padID", "html"]
, "getAttributePool" : ["padID"]
, "getRevisionsCount" : ["padID"]
, "getSavedRevisionsCount" : ["padID"]
, "listSavedRevisions" : ["padID"]
, "saveRevision" : ["padID", "rev"]
, "getRevisionChangeset" : ["padID", "rev"]
, "getLastEdited" : ["padID"]
, "deletePad" : ["padID"]
, "copyPad" : ["sourceID", "destinationID", "force"]
, "movePad" : ["sourceID", "destinationID", "force"]
, "getReadOnlyID" : ["padID"]
, "getPadID" : ["roID"]
, "setPublicStatus" : ["padID", "publicStatus"]
, "getPublicStatus" : ["padID"]
, "setPassword" : ["padID", "password"]
, "isPasswordProtected" : ["padID"]
, "listAuthorsOfPad" : ["padID"]
, "padUsersCount" : ["padID"]
, "getAuthorName" : ["authorID"]
, "padUsers" : ["padID"]
, "sendClientsMessage" : ["padID", "msg"]
, "listAllGroups" : []
, "checkToken" : []
, "appendChatMessage" : ["padID", "text", "authorID", "time"]
, "getChatHistory" : ["padID"]
, "getChatHistory" : ["padID", "start", "end"]
, "getChatHead" : ["padID"]
, "restoreRevision" : ["padID", "rev"]
}
};
// set the latest available API version here
exports.latestApiVersion = '1.2.11';
exports.latestApiVersion = '1.2.12';
// exports the versions so it can be used by the new Swagger endpoint
exports.version = version;

View File

@ -148,6 +148,9 @@ exports.doImport = function(req, res, padId)
if(!importHandledByPlugin || !directDatabaseAccess){
var fileEnding = path.extname(srcFile).toLowerCase();
var fileIsHTML = (fileEnding === ".html" || fileEnding === ".htm");
var fileIsTXT = (fileEnding === ".txt");
if (fileIsTXT) abiword = false; // Don't use abiword for text files
// See https://github.com/ether/etherpad-lite/issues/2572
if (abiword && !fileIsHTML) {
abiword.convertFile(srcFile, destFile, "htm", function(err) {
//catch convert errors
@ -213,7 +216,7 @@ exports.doImport = function(req, res, padId)
// Title needs to be stripped out else it appends it to the pad..
text = text.replace("<title>", "<!-- <title>");
text = text.replace("</title>","</title>-->");
//node on windows has a delay on releasing of the file lock.
//We add a 100ms delay to work around this
if(os.type().indexOf("Windows") > -1){
@ -245,7 +248,6 @@ exports.doImport = function(req, res, padId)
padManager.getPad(padId, function(err, _pad){
var pad = _pad;
padManager.unloadPad(padId);
// direct Database Access means a pad user should perform a switchToPad
// and not attempt to recieve updated pad data..
if(!directDatabaseAccess){

View File

@ -656,12 +656,17 @@ function handleUserChanges(data, cb)
, op
while(iterator.hasNext()) {
op = iterator.next()
if(op.opcode != '+') continue;
//+ can add text with attribs
//= can change or add attribs
//- can have attribs, but they are discarded and don't show up in the attribs - but do show up in the pool
op.attribs.split('*').forEach(function(attr) {
if(!attr) return
attr = wireApool.getAttrib(attr)
if(!attr) return
if('author' == attr[0] && attr[1] != thisSession.author) throw new Error("Trying to submit changes as another author in changeset "+changeset);
//the empty author is used in the clearAuthorship functionality so this should be the only exception
if('author' == attr[0] && (attr[1] != thisSession.author && attr[1] != '')) throw new Error("Trying to submit changes as another author in changeset "+changeset);
})
}
@ -1629,10 +1634,15 @@ function composePadChangesets(padId, startNum, endNum, callback)
changeset = changesets[startNum];
var pool = pad.apool();
for(var r=startNum+1;r<endNum;r++)
{
var cs = changesets[r];
changeset = Changeset.compose(changeset, cs, pool);
try {
for(var r=startNum+1;r<endNum;r++) {
var cs = changesets[r];
changeset = Changeset.compose(changeset, cs, pool);
}
} catch(e){
// r-1 indicates the rev that was build starting with startNum, applying startNum+1, +2, +3
console.warn("failed to compose cs in pad:",padId," startrev:",startNum," current rev:",r);
return callback(e);
}
callback(null);

View File

@ -13,6 +13,8 @@ exports.createServer = function () {
console.log("Report bugs at https://github.com/ether/etherpad-lite/issues")
serverName = "Etherpad " + settings.getGitCommit() + " (http://etherpad.org)";
console.log("Your Etherpad version is " + settings.getEpVersion() + " (" + settings.getGitCommit() + ")");
exports.restartServer();

View File

@ -17,7 +17,13 @@ exports.expressCreateServer = function (hook_name, args, cb) {
});
args.app.get('/admin/plugins/info', function(req, res) {
var gitCommit = settings.getGitCommit();
res.send( eejs.require("ep_etherpad-lite/templates/admin/plugins-info.html", {gitCommit:gitCommit}) );
var epVersion = settings.getEpVersion();
res.send( eejs.require("ep_etherpad-lite/templates/admin/plugins-info.html",
{
gitCommit: gitCommit,
epVersion: epVersion
})
);
});
}

View File

@ -284,6 +284,10 @@ var API = {
}
},
"response": {"chatHead":{"type":"Message"}}
},
"appendChatMessage": {
"func": "appendChatMessage",
"description": "appends a chat message"
}
}
};

View File

@ -145,7 +145,6 @@ function minify(req, res, next)
filename = path.normalize(path.join(ROOT_DIR, filename));
if (filename.indexOf(ROOT_DIR) == 0) {
filename = filename.slice(ROOT_DIR.length);
filename = filename.replace(/\\/g, '/'); // Windows (safe generally?)
} else {
res.writeHead(404, {});
res.end();

View File

@ -27,7 +27,7 @@ var npm = require("npm/lib/npm.js");
var jsonminify = require("jsonminify");
var log4js = require("log4js");
var randomString = require("./randomstring");
var suppressDisableMsg = " -- To suppress these warning messages change suppressErrorsInPadText to true in your settings.json\n";
/* Root path of the installation */
exports.root = path.normalize(path.join(npm.dir, ".."));
@ -54,6 +54,11 @@ exports.ip = "0.0.0.0";
*/
exports.port = process.env.PORT || 9001;
/**
* Should we suppress Error messages from being in Pad Contents
*/
exports.suppressErrorsInPadText = false;
/**
* The SSL signed server key and the Certificate Authority's own certificate
* default case: ep-lite does *not* use SSL. A signed server key is not required in this case.
@ -95,7 +100,7 @@ exports.toolbar = {
["showusers"]
],
timeslider: [
["timeslider_export", "timeslider_returnToPad"]
["timeslider_export", "timeslider_settings", "timeslider_returnToPad"]
]
}
@ -194,7 +199,6 @@ exports.getGitCommit = function() {
var refPath = rootPath + "/.git/" + ref.substring(5, ref.indexOf("\n"));
version = fs.readFileSync(refPath, "utf-8");
version = version.substring(0, 7);
console.log("Your Etherpad git version is " + version);
}
catch(e)
{
@ -203,6 +207,11 @@ exports.getGitCommit = function() {
return version;
}
// Return etherpad version from package.json
exports.getEpVersion = function() {
return require('ep_etherpad-lite/package.json').version;
}
exports.reloadSettings = function reloadSettings() {
// Discover where the settings file lives
var settingsFilename = argv.settings || "settings.json";
@ -266,7 +275,11 @@ exports.reloadSettings = function reloadSettings() {
{
fs.exists(exports.abiword, function(exists) {
if (!exists) {
console.error("Abiword does not exist at this path, check your settings file");
var abiwordError = "Abiword does not exist at this path, check your settings file";
if(!exports.suppressErrorsInPadText){
exports.defaultPadText = exports.defaultPadText + "\nError: " + abiwordError + suppressDisableMsg;
}
console.error(abiwordError);
exports.abiword = null;
}
});
@ -275,11 +288,19 @@ exports.reloadSettings = function reloadSettings() {
if(!exports.sessionKey){ // If the secretKey isn't set we also create yet another unique value here
exports.sessionKey = randomString(32);
console.warn("You need to set a sessionKey value in settings.json, this will allow your users to reconnect to your Etherpad Instance if your instance restarts");
var sessionWarning = "You need to set a sessionKey value in settings.json, this will allow your users to reconnect to your Etherpad Instance if your instance restarts";
if(!exports.suppressErrorsInPadText){
exports.defaultPadText = exports.defaultPadText + "\nWarning: " + sessionWarning + suppressDisableMsg;
}
console.warn(sessionWarning);
}
if(exports.dbType === "dirty"){
console.warn("DirtyDB is used. This is fine for testing but not recommended for production.");
var dirtyWarning = "DirtyDB is used. This is fine for testing but not recommended for production.";
if(!exports.suppressErrorsInPadText){
exports.defaultPadText = exports.defaultPadText + "\nWarning: " + dirtyWarning + suppressDisableMsg;
}
console.warn(dirtyWarning);
}
};

View File

@ -99,12 +99,14 @@ _.extend(Button.prototype, {
};
return tag("li", liAttributes,
tag("a", { "class": this.grouping, "data-l10n-id": this.attributes.localizationId },
tag("span", { "class": " "+ this.attributes.class })
tag("button", { "class": " "+ this.attributes.class, "data-l10n-id": this.attributes.localizationId })
)
);
}
});
SelectButton = function (attributes) {
this.attributes = attributes;
this.options = [];
@ -208,6 +210,12 @@ module.exports = {
class: "buttonicon buttonicon-import_export"
},
timeslider_settings: {
command: "settings",
localizationId: "pad.toolbar.settings.title",
class: "buttonicon buttonicon-settings"
},
timeslider_returnToPad: {
command: "timeslider_returnToPad",
localizationId: "timeslider.toolbar.returnbutton",

View File

@ -13,25 +13,25 @@
],
"dependencies" : {
"etherpad-yajsml" : "0.0.2",
"request" : "2.53.0",
"request" : "2.55.0",
"etherpad-require-kernel" : "1.0.8",
"resolve" : "1.1.0",
"socket.io" : "1.3.3",
"ueberDB" : "0.2.13",
"resolve" : "1.1.6",
"socket.io" : "1.3.5",
"ueberDB" : "0.2.15",
"express" : "3.8.1",
"async" : "0.9.0",
"connect" : "2.7.11",
"clean-css" : "3.0.8",
"uglify-js" : "2.4.16",
"formidable" : "1.0.16",
"clean-css" : "3.1.9",
"uglify-js" : "2.4.19",
"formidable" : "1.0.17",
"log4js" : "0.6.22",
"cheerio" : "0.18.0",
"cheerio" : "0.19.0",
"async-stacktrace" : "0.0.2",
"npm" : "2.4.1",
"npm" : "2.7.5",
"ejs" : "1.0.0",
"graceful-fs" : "3.0.5",
"graceful-fs" : "3.0.6",
"slide" : "1.1.6",
"semver" : "4.2.0",
"semver" : "4.3.3",
"security" : "1.0.0",
"tinycon" : "0.0.1",
"underscore" : "1.5.1",
@ -41,7 +41,7 @@
"channels" : "0.0.4",
"jsonminify" : "0.2.3",
"measured" : "1.0.0",
"mocha" : "2.1.0",
"mocha" : "2.2.1",
"supertest" : "0.15.0"
},
"bin": { "etherpad-lite": "./node/server.js" },
@ -54,5 +54,5 @@
"repository" : { "type" : "git",
"url" : "http://github.com/ether/etherpad-lite.git"
},
"version" : "1.5.2"
"version" : "1.5.3"
}

View File

@ -98,10 +98,26 @@ body.grayedout { background-color: #eee !important }
}
body.doesWrap {
white-space: pre-wrap; /*Must be pre-wrap to keep trailing spaces. Otherwise you get a zombie caret, walking around your screen (see #1766), WARNING: Enabling this causes Paste as plain text in Chrome to remove line breaks, this is probably undesirable */
/* white-space: pre-wrap; */
/*
Must be pre-wrap to keep trailing spaces. Otherwise you get a zombie caret,
walking around your screen (see #1766).
WARNING: Enabling this causes Paste as plain text in Chrome to remove line breaks
this is probably undesirable
WARNING: This causes copy & paste events to lose bold etc. attributes
NOTE: The walking-zombie caret issue seems to have been fixed in FF upstream
so let's try diabling pre-wrap and see how we get on now.
For more details see: https://github.com/ether/etherpad-lite/issues/2574
*/
word-wrap: break-word; /* fix for issue #1648 - firefox not wrapping long lines (without spaces) correctly */
}
body.doesWrap > div{
/* Related to #1766 */
white-space: pre-wrap;
}
#innerdocbody {
padding-top: 1px; /* important for some reason? */
padding-right: 10px;

View File

@ -70,10 +70,6 @@ a img {
.toolbar ul li {
float: left;
margin-left: 2px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
height:32px;
}
.toolbar ul li.separator {
@ -141,9 +137,24 @@ a img {
top: 1px;
}
.toolbar ul li a .buttontext {
color: #222;
color: #666;
font-size: 14px;
border:none;
background:none;
margin-top:1px;
color:#666;
}
.buttontext::-moz-focus-inner {
padding: 0;
border: 0;
}
.buttontext:focus{
/* Not sure why important is required here but it is */
border: 1px solid #666 !important;
}
.toolbar ul li a.grouped-left {
border-radius: 3px 0 0 3px;
}
@ -197,6 +208,7 @@ li[data-key=showusers] > a #online_count {
#editbar{
display:none;
}
#editorcontainer {
position: absolute;
top: 37px; /* + 1px border */
@ -742,12 +754,24 @@ table#otheruserstable {
height: 16px;
display: inline-block;
vertical-align: middle;
border: none;
padding: 0;
background: none;
font-family: "fontawesome-etherpad";
font-size: 15px;
font-style: normal;
font-weight: normal;
color: #666;
cursor: pointer;
}
.buttonicon::-moz-focus-inner {
padding: 0;
border: 0
}
.buttonicon:focus{
border: 1px solid #666;
}
.buttonicon-bold:before {
content: "\e81c";
@ -1216,6 +1240,11 @@ input[type=checkbox] {
}
/* End of gritter stuff */
@font-face {
font-family: opendyslexic;
src: url("../../static/font/opendyslexic.otf") format("opentype");
}
@font-face {
font-family: "fontawesome-etherpad";
src:url("../font/fontawesome-etherpad.eot");
@ -1254,3 +1283,11 @@ input[type=checkbox] {
-moz-osx-font-smoothing: grayscale;
}
.hideControlsEditor{
top:0px !important;
}
.hideControlsEditbar{
display:none !important;
}

View File

@ -78,6 +78,7 @@
width: 44px;
text-align:center;
vertical-align:middle;
background:none;
}
#playpause_button {
right: 77px;
@ -125,7 +126,7 @@
font-family: fontawesome-etherpad;
border-radius:2px;
border: #666 solid 1px;
line-height:18px;
/* line-height:18px; */
text-align:center;
height:22px;
color:#666;
@ -204,12 +205,9 @@ stepper:active{
float:right;
height:30px;
}
#settings,
#import_export,
#embed,
#connectivity,
#users {
top: 62px;
#import_export, #settings{
top: 115px;
position: fixed;
}
#import_export .popup {
width: 183px;
@ -218,9 +216,7 @@ stepper:active{
border-radius: 0 0 0 6px;
}
#import_export {
top: 115px;
width: 185px;
position: fixed;
}
.timeslider-bar {
background: #f7f7f7;
@ -236,7 +232,7 @@ stepper:active{
.timeslider-bar #editbar {
border-bottom: none;
float: right;
width: 170px;
width: 180px;
}
.timeslider-bar h1 {
margin: 5px
@ -337,3 +333,19 @@ OL {
.list-number6 {
list-style-type: lower-roman
}
button{
margin:0;
padding:0;
cursor:pointer;
}
button::-moz-focus-inner {
padding: 0;
border: 0
}
button:focus{
border: 1px solid #666;
}

Binary file not shown.

View File

@ -98,7 +98,7 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
/*
Gets all attributes on a line
@param lineNum: the number of the line to set the attribute for
@param lineNum: the number of the line to get the attribute for
*/
getAttributesOnLine: function(lineNum){
// get attributes of first char of line
@ -122,6 +122,59 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
return [];
},
/*
Gets all attributes at a position containing line number and column
@param lineNumber starting with zero
@param column starting with zero
returns a list of attributes in the format
[ ["key","value"], ["key","value"], ... ]
*/
getAttributesOnPosition: function(lineNumber, column){
// get all attributes of the line
var aline = this.rep.alines[lineNumber];
if (!aline) {
return [];
}
// iterate through all operations of a line
var opIter = Changeset.opIterator(aline);
// we need to sum up how much characters each operations take until the wanted position
var currentPointer = 0;
var attributes = [];
var currentOperation;
while (opIter.hasNext()) {
currentOperation = opIter.next();
currentPointer = currentPointer + currentOperation.chars;
if (currentPointer > column) {
// we got the operation of the wanted position, now collect all its attributes
Changeset.eachAttribNumber(currentOperation.attribs, function (n) {
attributes.push([
this.rep.apool.getAttribKey(n),
this.rep.apool.getAttribValue(n)
]);
}.bind(this));
// skip the loop
return attributes;
}
}
return attributes;
},
/*
Gets all attributes at caret position
if the user selected a range, the start of the selection is taken
returns a list of attributes in the format
[ ["key","value"], ["key","value"], ... ]
*/
getAttributesOnCaret: function(){
return this.getAttributesOnPosition(this.rep.selStart[0], this.rep.selStart[1]);
},
/*
Sets a specified attribute on a line
@param lineNum: the number of the line to set the attribute for
@ -153,53 +206,58 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
return this.applyChangeset(builder);
},
/*
Removes a specified attribute on a line
@param lineNum: the number of the affected line
@param attributeKey: the name of the attribute to remove, e.g. list
/**
* Removes a specified attribute on a line
* @param lineNum the number of the affected line
* @param attributeName the name of the attribute to remove, e.g. list
* @param attributeValue if given only attributes with equal value will be removed
*/
removeAttributeOnLine: function(lineNum, attributeName, attributeValue){
var loc = [0,0];
var builder = Changeset.builder(this.rep.lines.totalWidth());
var hasMarker = this.lineHasMarker(lineNum);
var attribs
var foundAttrib = false
attribs = this.getAttributesOnLine(lineNum).map(function(attrib) {
if(attrib[0] === attributeName) {
foundAttrib = true
return [attributeName, null] // remove this attrib from the linemarker
}
return attrib
})
removeAttributeOnLine: function(lineNum, attributeName, attributeValue){
var builder = Changeset.builder(this.rep.lines.totalWidth());
var hasMarker = this.lineHasMarker(lineNum);
var found = false;
if(!foundAttrib) {
return
var attribs = _(this.getAttributesOnLine(lineNum)).map(function (attrib) {
if (attrib[0] === attributeName && (!attributeValue || attrib[0] === attributeValue)){
found = true;
return [attributeName, ''];
}
return attrib;
});
if(hasMarker){
ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0]));
// If length == 4, there's [author, lmkr, insertorder, + the attrib being removed] thus we can remove the marker entirely
if(attribs.length <= 4) ChangesetUtils.buildRemoveRange(this.rep, builder, loc, (loc = [lineNum, 1]))
else ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 1]), attribs, this.rep.apool);
}
return this.applyChangeset(builder);
},
if (!found) {
return;
}
ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]);
var countAttribsWithMarker = _.chain(attribs).filter(function(a){return !!a[1];})
.map(function(a){return a[0];}).difference(['author', 'lmkr', 'insertorder', 'start']).size().value();
//if we have marker and any of attributes don't need to have marker. we need delete it
if(hasMarker && !countAttribsWithMarker){
ChangesetUtils.buildRemoveRange(this.rep, builder, [lineNum, 0], [lineNum, 1]);
}else{
ChangesetUtils.buildKeepRange(this.rep, builder, [lineNum, 0], [lineNum, 1], attribs, this.rep.apool);
}
return this.applyChangeset(builder);
},
/*
Sets a specified attribute on a line
@param lineNum: the number of the line to set the attribute for
@param attributeKey: the name of the attribute to set, e.g. list
@param attributeValue: an optional parameter to pass to the attribute (e.g. indention level)
Toggles a line attribute for the specified line number
If a line attribute with the specified name exists with any value it will be removed
Otherwise it will be set to the given value
@param lineNum: the number of the line to toggle the attribute for
@param attributeKey: the name of the attribute to toggle, e.g. list
@param attributeValue: the value to pass to the attribute (e.g. indention level)
*/
toggleAttributeOnLine: function(lineNum, attributeName, attributeValue) {
return this.getAttributeOnLine(attributeName) ?
return this.getAttributeOnLine(lineNum, attributeName) ?
this.removeAttributeOnLine(lineNum, attributeName) :
this.setAttributeOnLine(lineNum, attributeName, attributeValue);
}
});
module.exports = AttributeManager;
module.exports = AttributeManager;

View File

@ -265,7 +265,7 @@ plugins.ensure(function () {\n\
iframeHTML: iframeHTML
});
iframeHTML.push('</head><body id="innerdocbody" class="syntax" spellcheck="false">&nbsp;</body></html>');
iframeHTML.push('</head><body id="innerdocbody" role="application" class="syntax" spellcheck="false">&nbsp;</body></html>');
// Expose myself to global for my child frame.
var thisFunctionsName = "ChildAccessibleAce2Editor";
@ -279,6 +279,7 @@ window.onload = function () {\n\
setTimeout(function () {\n\
var iframe = document.createElement("IFRAME");\n\
iframe.name = "ace_inner";\n\
iframe.title = "pad";\n\
iframe.scrolling = "no";\n\
var outerdocbody = document.getElementById("outerdocbody");\n\
iframe.frameBorder = 0;\n\
@ -319,6 +320,7 @@ window.onload = function () {\n\
var outerFrame = document.createElement("IFRAME");
outerFrame.name = "ace_outer";
outerFrame.frameBorder = 0; // for IE
outerFrame.title = "Ether";
info.frame = outerFrame;
document.getElementById(containerId).appendChild(outerFrame);

View File

@ -608,8 +608,11 @@ function Ace2Inner(){
// Chrome can't handle the truth.. If CSS rule white-space:pre-wrap
// is true then any paste event will insert two lines..
// Sadly this will mean you get a walking Caret in Chrome when clicking on a URL
// So this has to be set to pre-wrap ;(
// We need to file a bug w/ the Chromium team.
if(browser.chrome){
$("#innerdocbody").css({"white-space":"normal"});
$("#innerdocbody").css({"white-space":"pre-wrap"});
}
}
@ -2322,93 +2325,72 @@ function Ace2Inner(){
}
editorInfo.ace_setAttributeOnSelection = setAttributeOnSelection;
function getAttributeOnSelection(attributeName){
if (!(rep.selStart && rep.selEnd)) return;
// get the previous/next characters formatting when we have nothing selected
// To fix this we just change the focus area, we don't actually check anything yet.
if(rep.selStart[1] == rep.selEnd[1]){
// if we're at the beginning of a line bump end forward so we get the right attribute
if(rep.selStart[1] == 0 && rep.selEnd[1] == 0){
rep.selEnd[1] = 1;
}
if(rep.selStart[1] < 0){
rep.selStart[1] = 0;
}
var line = rep.lines.atIndex(rep.selStart[0]);
// if we're at the end of the line bmp the start back 1 so we get hte attribute
if(rep.selEnd[1] == line.text.length){
rep.selStart[1] = rep.selStart[1] -1;
}
}
// Do the detection
var selectionAllHasIt = true;
if (!(rep.selStart && rep.selEnd)) return
var withIt = Changeset.makeAttribsString('+', [
[attributeName, 'true']
], rep.apool);
var withItRegex = new RegExp(withIt.replace(/\*/g, '\\*') + "(\\*|$)");
function hasIt(attribs)
{
return withItRegex.test(attribs);
}
var selStartLine = rep.selStart[0];
var selEndLine = rep.selEnd[0];
for (var n = selStartLine; n <= selEndLine; n++)
{
var opIter = Changeset.opIterator(rep.alines[n]);
var indexIntoLine = 0;
var selectionStartInLine = 0;
var selectionEndInLine = rep.lines.atIndex(n).text.length; // exclude newline
if(rep.lines.atIndex(n).text.length == 0){
return false; // If the line length is 0 we basically treat it as having no formatting
return rangeHasAttrib(rep.selStart, rep.selEnd)
function rangeHasAttrib(selStart, selEnd) {
// if range is collapsed -> no attribs in range
if(selStart[1] == selEnd[1] && selStart[0] == selEnd[0]) return false
if(selStart[0] != selEnd[0]) { // -> More than one line selected
var hasAttrib = true
// from selStart to the end of the first line
hasAttrib = hasAttrib && rangeHasAttrib(selStart, [selStart[0], rep.lines.atIndex(selStart[0]).text.length])
// for all lines in between
for(var n=selStart[0]+1; n < selEnd[0]; n++) {
hasAttrib = hasAttrib && rangeHasAttrib([n, 0], [n, rep.lines.atIndex(n).text.length])
}
// for the last, potentially partial, line
hasAttrib = hasAttrib && rangeHasAttrib([selEnd[0], 0], [selEnd[0], selEnd[1]])
return hasAttrib
}
if(rep.selStart[1] == rep.selEnd[1] && rep.selStart[1] == rep.lines.atIndex(n).text.length){
return false; // If we're at the end of a line we treat it as having no formatting
}
if(rep.selStart[1] == 0 && rep.selEnd[1] == 0){
rep.selEnd[1] == 1;
}
if(rep.selEnd[1] == -1){
rep.selEnd[1] = 1; // sometimes rep.selEnd is -1, not sure why.. When it is we should look at the first char
}
if (n == selStartLine)
{
selectionStartInLine = rep.selStart[1];
}
if (n == selEndLine)
{
selectionEndInLine = rep.selEnd[1];
}
while (opIter.hasNext())
{
// Logic tells us we now have a range on a single line
var lineNum = selStart[0]
, start = selStart[1]
, end = selEnd[1]
, hasAttrib = true
// Iterate over attribs on this line
var opIter = Changeset.opIterator(rep.alines[lineNum])
, indexIntoLine = 0
while (opIter.hasNext()) {
var op = opIter.next();
var opStartInLine = indexIntoLine;
var opEndInLine = opStartInLine + op.chars;
if (!hasIt(op.attribs))
{
if (!hasIt(op.attribs)) {
// does op overlap selection?
if (!(opEndInLine <= selectionStartInLine || opStartInLine >= selectionEndInLine))
{
selectionAllHasIt = false;
if (!(opEndInLine <= start || opStartInLine >= end)) {
hasAttrib = false; // since it's overlapping but hasn't got the attrib -> range hasn't got it
break;
}
}
indexIntoLine = opEndInLine;
}
if (!selectionAllHasIt)
{
break;
}
}
if(selectionAllHasIt){
return true;
}else{
return false;
return hasAttrib
}
}
editorInfo.ace_getAttributeOnSelection = getAttributeOnSelection;
function toggleAttributeOnSelection(attributeName)
@ -3636,6 +3618,8 @@ function Ace2Inner(){
var charCode = evt.charCode;
var keyCode = evt.keyCode;
var which = evt.which;
var altKey = evt.altKey;
var shiftKey = evt.shiftKey;
// prevent ESC key
if (keyCode == 27)
@ -3676,7 +3660,6 @@ function Ace2Inner(){
if (keyCode == 13 && browser.opera && (type == "keypress")){
return; // This stops double enters in Opera but double Tabs still show on single tab keypress, adding keyCode == 9 to this doesn't help as the event is fired twice
}
var specialHandled = false;
var isTypeForSpecialKey = ((browser.msie || browser.safari || browser.chrome) ? (type == "keydown") : (type == "keypress"));
var isTypeForCmdKey = ((browser.msie || browser.safari || browser.chrome) ? (type == "keydown") : (type == "keypress"));
@ -3707,6 +3690,101 @@ function Ace2Inner(){
evt:evt
});
specialHandled = (specialHandledInHook&&specialHandledInHook.length>0)?specialHandledInHook[0]:specialHandled;
if ((!specialHandled) && altKey && isTypeForSpecialKey && keyCode == 120){
// Alt F9 focuses on the File Menu and/or editbar.
// Note that while most editors use Alt F10 this is not desirable
// As ubuntu cannot use Alt F10....
// Focus on the editbar. -- TODO: Move Focus back to previous state (we know it so we can use it)
var firstEditbarElement = parent.parent.$('#editbar').children("ul").first().children().first().children().first().children().first();
$(this).blur();
firstEditbarElement.focus();
evt.preventDefault();
}
if ((!specialHandled) && altKey && keyCode == 67){
// Alt c focuses on the Chat window
$(this).blur();
parent.parent.chat.show();
parent.parent.chat.focus();
evt.preventDefault();
}
if ((!specialHandled) && evt.ctrlKey && shiftKey && keyCode == 50 && type === "keydown"){
// Control-Shift-2 shows a gritter popup showing a line author
var lineNumber = rep.selEnd[0];
var alineAttrs = rep.alines[lineNumber];
var apool = rep.apool;
// TODO: support selection ranges
// TODO: Still work when authorship colors have been cleared
// TODO: i18n
// TODO: There appears to be a race condition or so.
var author = null;
if (alineAttrs) {
var authors = [];
var authorNames = [];
var opIter = Changeset.opIterator(alineAttrs);
while (opIter.hasNext()){
var op = opIter.next();
authorId = Changeset.opAttributeValue(op, 'author', apool);
// Only push unique authors and ones with values
if(authors.indexOf(authorId) === -1 && authorId !== ""){
authors.push(authorId);
}
}
}
// No author information is available IE on a new pad.
if(authors.length === 0){
var authorString = "No author information is available";
}
else{
// Known authors info, both current and historical
var padAuthors = parent.parent.pad.userList();
var authorObj = {};
authors.forEach(function(authorId){
padAuthors.forEach(function(padAuthor){
// If the person doing the lookup is the author..
if(padAuthor.userId === authorId){
if(parent.parent.clientVars.userId === authorId){
authorObj = {
name: "Me"
}
}else{
authorObj = padAuthor;
}
}
});
if(!authorObj){
author = "Unknown";
return;
}
author = authorObj.name;
if(!author) author = "Unknown";
authorNames.push(author);
})
}
if(authors.length === 1){
var authorString = "The author of this line is " + authorNames;
}
if(authors.length > 1){
var authorString = "The authors of this line are " + authorNames.join(" & ");
}
parent.parent.$.gritter.add({
// (string | mandatory) the heading of the notification
title: 'Line Authors',
// (string | mandatory) the text inside the notification
text: authorString,
// (bool | optional) if you want it to fade out on its own or just sit there
sticky: false,
// (int | optional) the time you want it to be alive for before fading out
time: '4000'
});
}
if ((!specialHandled) && isTypeForSpecialKey && keyCode == 8)
{
// "delete" key; in mozilla, if we're at the beginning of a line, normalize now,
@ -4863,7 +4941,11 @@ function Ace2Inner(){
$(document).on("keypress", handleKeyEvent);
$(document).on("keyup", handleKeyEvent);
$(document).on("click", handleClick);
$(document).on("cut", handleCut);
// Disabled: https://github.com/ether/etherpad-lite/issues/2546
// Will break OL re-numbering: https://github.com/ether/etherpad-lite/pull/2533
// $(document).on("cut", handleCut);
$(root).on("blur", handleBlur);
if (browser.msie)
{

View File

@ -290,6 +290,11 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded)
$(document).keyup(function(e)
{
// If focus is on editbar, don't do anything
var target = $(':focus');
if($(target).parents(".toolbar").length === 1){
return;
}
var code = -1;
if (!e) var e = window.event;
if (e.keyCode) code = e.keyCode;
@ -330,7 +335,6 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded)
}
}
else if (code == 32) playpause();
});
$(window).resize(function()

View File

@ -18,6 +18,7 @@ var padutils = require('./pad_utils').padutils;
var padcookie = require('./pad_cookie').padcookie;
var Tinycon = require('tinycon/tinycon');
var hooks = require('./pluginfw/hooks');
var padeditor = require('./pad_editor').padeditor;
var chat = (function()
{
@ -36,6 +37,14 @@ var chat = (function()
chatMentions = 0;
Tinycon.setBubble(0);
},
focus: function ()
{
// I'm not sure why we need a setTimeout here but without it we don't get focus...
// Animation maybe?
setTimeout(function(){
$("#chatinput").focus();
},100);
},
stickToScreen: function(fromInitialCall) // Make chat stick to right hand side of screen
{
chat.show();
@ -205,8 +214,28 @@ var chat = (function()
init: function(pad)
{
this._pad = pad;
$("#chatinput").keypress(function(evt)
$("#chatinput").keyup(function(evt)
{
// If the event is Alt C or Escape & we're already in the chat menu
// Send the users focus back to the pad
if((evt.altKey == true && evt.which === 67) || evt.which === 27){
// If we're in chat already..
$(':focus').blur(); // required to do not try to remove!
padeditor.ace.focus(); // Sends focus back to pad
}
});
$('body:not(#chatinput)').on("keydown", function(evt){
if (evt.altKey && evt.which == 67){
// Alt c focuses on the Chat window
$(this).blur();
parent.parent.chat.show();
parent.parent.chat.focus();
evt.preventDefault();
}
});
$("#chatinput").keypress(function(evt){
//if the user typed enter, fire the send
if(evt.which == 13 || evt.which == 10)
{

View File

@ -297,7 +297,23 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas
{
if (state.attribs[a])
{
lst.push([a, 'true']);
// The following splitting of the attribute name is a workaround
// to enable the content collector to store key-value attributes
// see https://github.com/ether/etherpad-lite/issues/2567 for more information
// in long term the contentcollector should be refactored to get rid of this workaround
var ATTRIBUTE_SPLIT_STRING = "::";
// see if attributeString is splittable
var attributeSplits = a.split(ATTRIBUTE_SPLIT_STRING);
if (attributeSplits.length > 1) {
// the attribute name follows the convention key::value
// so save it as a key value attribute
lst.push([attributeSplits[0], attributeSplits[1]]);
} else {
// the "normal" case, the attribute is just a switch
// so set it true
lst.push([a, 'true']);
}
}
}
if (state.authorLevel > 0)
@ -571,7 +587,9 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas
}
else if ((tname == "div" || tname == "p") && cls && cls.match(/(?:^| )ace-line\b/))
{
oldListTypeOrNull = (_enterList(state, type) || 'none');
// This has undesirable behavior in Chrome but is right in other browsers.
// See https://github.com/ether/etherpad-lite/issues/2412 for reasoning
if(!abrowser.chrome) oldListTypeOrNull = (_enterList(state, type) || 'none');
}
if (className2Author && cls)
{

View File

@ -78,7 +78,7 @@
_tpl_close: '<div class="gritter-close"></div>',
_tpl_title: '<span class="gritter-title">[[title]]</span>',
_tpl_item: '<div id="gritter-item-[[number]]" class="gritter-item-wrapper [[item_class]]" style="display:none"><div class="gritter-top"></div><div class="gritter-item">[[close]][[image]]<div class="[[class_name]]">[[title]]<p>[[text]]</p></div><div style="clear:both"></div></div><div class="gritter-bottom"></div></div>',
_tpl_wrap: '<div id="gritter-notice-wrapper"></div>',
_tpl_wrap: '<div id="gritter-notice-wrapper" aria-live="polite" aria-atomic="false" aria-relevant="additions" role="log"></div>',
/**
* Add a gritter notification to the screen

View File

@ -110,7 +110,7 @@ function randomString()
// callback: the function to call when all above succeeds, `val` is the value supplied by the user
var getParameters = [
{ name: "noColors", checkVal: "true", callback: function(val) { settings.noColors = true; $('#clearAuthorship').hide(); } },
{ name: "showControls", checkVal: "false", callback: function(val) { $('#editbar').hide(); $('#editorcontainer').css({"top":"0px"}); } },
{ name: "showControls", checkVal: "false", callback: function(val) { $('#editbar').addClass('hideControlsEditbar'); $('#editorcontainer').addClass('hideControlsEditor'); } },
{ name: "showChat", checkVal: "false", callback: function(val) { $('#chaticon').hide(); } },
{ name: "showLineNumbers", checkVal: "false", callback: function(val) { settings.LineNumbersDisabled = true; } },
{ name: "useMonospaceFont", checkVal: "true", callback: function(val) { settings.useMonospaceFontGlobal = true; } },
@ -433,6 +433,10 @@ var pad = {
{
return pad.myUserInfo.name;
},
userList: function()
{
return paduserlist.users();
},
sendClientReady: function(isReconnect, messageType)
{
messageType = typeof messageType !== 'undefined' ? messageType : 'CLIENT_READY';
@ -576,9 +580,18 @@ var pad = {
if(padcookie.getPref("rtlIsTrue") == true){
pad.changeViewOption('rtlIsTrue', true);
}
if(padcookie.getPref("useMonospaceFont") == true){
pad.changeViewOption('useMonospaceFont', true);
}
var fonts = ['useMonospaceFont', 'useOpenDyslexicFont', 'useComicSansFont', 'useCourierNewFont', 'useGeorgiaFont', 'useImpactFont',
'useLucidaFont', 'useLucidaSansFont', 'usePalatinoFont', 'useTahomaFont', 'useTimesNewRomanFont',
'useTrebuchetFont', 'useVerdanaFont', 'useSymbolFont', 'useWebdingsFont', 'useWingDingsFont', 'useSansSerifFont',
'useSerifFont'];
$.each(fonts, function(i, font){
if(padcookie.getPref(font) == true){
pad.changeViewOption(font, true);
}
})
hooks.aCallAll("postAceInit", {ace: padeditor.ace, pad: pad});
}
},

View File

@ -63,6 +63,7 @@ ToolbarItem.prototype.bind = function (callback) {
if (self.isButton()) {
self.$el.click(function (event) {
$(':focus').blur();
callback(self.getCommand(), self);
event.preventDefault();
});
@ -155,6 +156,10 @@ var padeditbar = (function()
});
});
$('body:not(#editorcontainerbox)').on("keydown", function(evt){
bodyKeyEvent(evt);
});
$('#editbar').show();
this.redrawHeight();
@ -300,6 +305,72 @@ var padeditbar = (function()
}
};
var editbarPosition = 0;
function bodyKeyEvent(evt){
// If the event is Alt F9 or Escape & we're already in the editbar menu
// Send the users focus back to the pad
if((evt.keyCode === 120 && evt.altKey) || evt.keyCode === 27){
if($(':focus').parents(".toolbar").length === 1){
// If we're in the editbar already..
// Close any dropdowns we have open..
padeditbar.toggleDropDown("none");
// Check we're on a pad and not on the timeslider
// Or some other window I haven't thought about!
if(typeof pad === 'undefined'){
// Timeslider probably..
// Shift focus away from any drop downs
$(':focus').blur(); // required to do not try to remove!
$('#padmain').focus(); // Focus back onto the pad
}else{
// Shift focus away from any drop downs
$(':focus').blur(); // required to do not try to remove!
padeditor.ace.focus(); // Sends focus back to pad
// The above focus doesn't always work in FF, you have to hit enter afterwards
evt.preventDefault();
}
}else{
// Focus on the editbar :)
var firstEditbarElement = parent.parent.$('#editbar').children("ul").first().children().first().children().first().children().first();
$(this).blur();
firstEditbarElement.focus();
evt.preventDefault();
}
}
// Are we in the toolbar??
if($(':focus').parents(".toolbar").length === 1){
// On arrow keys go to next/previous button item in editbar
if(evt.keyCode !== 39 && evt.keyCode !== 37) return;
// Get all the focusable items in the editbar
var focusItems = $('#editbar').find('button, select');
// On left arrow move to next button in editbar
if(evt.keyCode === 37){
// If a dropdown is visible or we're in an input don't move to the next button
if($('.popup').is(":visible") || evt.target.localName === "input") return;
editbarPosition--;
// Allow focus to shift back to end of row and start of row
if(editbarPosition === -1) editbarPosition = focusItems.length -1;
$(focusItems[editbarPosition]).focus()
}
// On right arrow move to next button in editbar
if(evt.keyCode === 39){
// If a dropdown is visible or we're in an input don't move to the next button
if($('.popup').is(":visible") || evt.target.localName === "input") return;
editbarPosition++;
// Allow focus to shift back to end of row and start of row
if(editbarPosition >= focusItems.length) editbarPosition = 0;
$(focusItems[editbarPosition]).focus();
}
}
}
function aceAttributeCommand(cmd, ace) {
ace.ace_toggleAttributeOnSelection(cmd);
}
@ -311,10 +382,36 @@ var padeditbar = (function()
toolbar.registerDropdownCommand("import_export");
toolbar.registerDropdownCommand("embed");
toolbar.registerCommand("settings", function () {
toolbar.toggleDropDown("settings", function(){
$('#options-stickychat').focus();
});
});
toolbar.registerCommand("import_export", function () {
toolbar.toggleDropDown("import_export", function(){
// If Import file input exists then focus on it..
if($('#importfileinput').length !== 0){
setTimeout(function(){
$('#importfileinput').focus();
}, 100);
}else{
$('.exportlink').first().focus();
}
});
});
toolbar.registerCommand("showusers", function () {
toolbar.toggleDropDown("users", function(){
$('#myusernameedit').focus();
});
});
toolbar.registerCommand("embed", function () {
toolbar.setEmbedLinks();
$('#linkinput').focus().select();
toolbar.toggleDropDown("embed");
toolbar.toggleDropDown("embed", function(){
$('#linkinput').focus().select();
});
});
toolbar.registerCommand("savedRevision", function () {

View File

@ -28,6 +28,13 @@ var padeditor = (function()
var Ace2Editor = undefined;
var pad = undefined;
var settings = undefined;
// Array of available fonts
var fonts = ['useMonospaceFont', 'useOpenDyslexicFont', 'useComicSansFont', 'useCourierNewFont', 'useGeorgiaFont', 'useImpactFont',
'useLucidaFont', 'useLucidaSansFont', 'usePalatinoFont', 'useTahomaFont', 'useTimesNewRomanFont',
'useTrebuchetFont', 'useVerdanaFont', 'useSymbolFont', 'useWebdingsFont', 'useWingDingsFont', 'useSansSerifFont',
'useSerifFont'];
var self = {
ace: null,
// this is accessed directly from other files
@ -85,10 +92,15 @@ var padeditor = (function()
padutils.setCheckbox($("#options-rtlcheck"), ('rtl' == html10n.getDirection()));
})
// font face
// font family change
$("#viewfontmenu").change(function()
{
pad.changeViewOption('useMonospaceFont', $("#viewfontmenu").val() == 'monospace');
$.each(fonts, function(i, font){
var sfont = font.replace("use","");
sfont = sfont.replace("Font","");
sfont = sfont.toLowerCase();
pad.changeViewOption(font, $("#viewfontmenu").val() == sfont);
});
});
// Language
@ -98,12 +110,12 @@ var padeditor = (function()
// this does not interfere with html10n's normal value-setting because html10n just ingores <input>s
// also, a value which has been set by the user will be not overwritten since a user-edited <input>
// does *not* have the editempty-class
$('input[data-l10n-id]').each(function(key, input)
{
input = $(input);
if(input.hasClass("editempty"))
input.val(html10n.get(input.attr("data-l10n-id")));
});
$('input[data-l10n-id]').each(function(key, input){
input = $(input);
if(input.hasClass("editempty")){
input.val(html10n.get(input.attr("data-l10n-id")));
}
});
})
$("#languagemenu").val(html10n.getLanguage());
$("#languagemenu").change(function() {
@ -136,13 +148,49 @@ var padeditor = (function()
v = getOption('showAuthorColors', true);
self.ace.setProperty("showsauthorcolors", v);
padutils.setCheckbox($("#options-colorscheck"), v);
// Override from parameters if true
if (settings.noColors !== false)
self.ace.setProperty("showsauthorcolors", !settings.noColors);
v = getOption('useMonospaceFont', false);
self.ace.setProperty("textface", (v ? "monospace" : "Arial, sans-serif"));
$("#viewfontmenu").val(v ? "monospace" : "normal");
// Override from parameters if true
if (settings.noColors !== false){
self.ace.setProperty("showsauthorcolors", !settings.noColors);
}
var normalFont = true;
// Go through each font and see if the option is set..
$.each(fonts, function(i, font){
var isEnabled = getOption(font, false);
if(isEnabled){
font = font.replace("use","");
font = font.replace("Font","");
font = font.toLowerCase();
if(font === "monospace") self.ace.setProperty("textface", "Courier new");
if(font === "opendyslexic") self.ace.setProperty("textface", "OpenDyslexic");
if(font === "comicsans") self.ace.setProperty("textface", "Comic Sans MS");
if(font === "georgia") self.ace.setProperty("textface", "Georgia");
if(font === "impact") self.ace.setProperty("textface", "Impact");
if(font === "lucida") self.ace.setProperty("textface", "Lucida");
if(font === "lucidasans") self.ace.setProperty("textface", "Lucida Sans Unicode");
if(font === "palatino") self.ace.setProperty("textface", "Palatino Linotype");
if(font === "tahoma") self.ace.setProperty("textface", "Tahoma");
if(font === "timesnewroman") self.ace.setProperty("textface", "Times New Roman");
if(font === "trebuchet") self.ace.setProperty("textface", "Trebuchet MS");
if(font === "verdana") self.ace.setProperty("textface", "Verdana");
if(font === "symbol") self.ace.setProperty("textface", "Symbol");
if(font === "webdings") self.ace.setProperty("textface", "Webdings");
if(font === "wingdings") self.ace.setProperty("textface", "Wingdings");
if(font === "sansserif") self.ace.setProperty("textface", "MS Sans Serif");
if(font === "serif") self.ace.setProperty("textface", "MS Serif");
// $("#viewfontmenu").val(font);
normalFont = false;
}
});
// No font has been previously selected so use the Normal font
if(normalFont){
self.ace.setProperty("textface", "Arial, sans-serif");
// $("#viewfontmenu").val("normal");
}
},
dispose: function()
{

View File

@ -508,6 +508,30 @@ var paduserlist = (function()
});
//
},
users: function(){
// Returns an object of users who have been on this pad
// Firstly we have to get live data..
var userList = otherUsersInfo;
// Now we need to add ourselves..
userList.push(myUserInfo);
// Now we add historical authors
var historical = clientVars.collab_client_vars.historicalAuthorData;
for (var key in historical){
var userId = historical[key].userId;
// Check we don't already have this author in our array
var exists = false;
userList.forEach(function(user){
if(user.userId === userId) exists = true;
});
if(exists === false){
userList.push(historical[key]);
}
}
return userList;
},
setMyUserInfo: function(info)
{
//translate the colorId

View File

@ -157,6 +157,38 @@ function handleClientVars(message)
fireWhenAllScriptsAreLoaded[i]();
}
$("#ui-slider-handle").css('left', $("#ui-slider-bar").width() - 2);
// Translate some strings where we only want to set the title not the actual values
$('#playpause_button_icon').attr("title", html10n.get("timeslider.playPause"));
$('#leftstep').attr("title", html10n.get("timeslider.backRevision"));
$('#rightstep').attr("title", html10n.get("timeslider.forwardRevision"));
// font family change
$("#viewfontmenu").change(function(){
var font = $("#viewfontmenu").val();
if(font === "monospace") setFont("Courier new");
if(font === "opendyslexic") setFont("OpenDyslexic");
if(font === "comicsans") setFont("Comic Sans MS");
if(font === "georgia") setFont("Georgia");
if(font === "impact") setFont("Impact");
if(font === "lucida") setFont("Lucida");
if(font === "lucidasans") setFont("Lucida Sans Unicode");
if(font === "palatino") setFont("Palatino Linotype");
if(font === "tahoma") setFont("Tahoma");
if(font === "timesnewroman") setFont("Times New Roman");
if(font === "trebuchet") setFont("Trebuchet MS");
if(font === "verdana") setFont("Verdana");
if(font === "symbol") setFont("Symbol");
if(font === "webdings") setFont("Webdings");
if(font === "wingdings") setFont("Wingdings");
if(font === "sansserif") setFont("MS Sans Serif");
if(font === "serif") setFont("MS Serif");
});
}
function setFont(font){
$('#padcontent').css("font-family", font);
}
exports.baseURL = '';

View File

@ -22,8 +22,9 @@
</div>
<div class="innerwrapper">
<h2>Etherpad Git Commit</h2>
<p><a href='https://github.com/ether/etherpad-lite/commit/<%= gitCommit %>'><%= gitCommit %></a></p>
<h2>Etherpad version</h2>
<p>Version number: <%= epVersion %></p>
<p>Git sha: <a href='https://github.com/ether/etherpad-lite/commit/<%= gitCommit %>'><%= gitCommit %></a></p>
<h2>Installed plugins</h2>
<pre><%- plugins.formatPlugins().replace(", ","\n") %></pre>

View File

@ -70,9 +70,10 @@
}
#button {
margin: 0 auto;
border-radius: 3px;
text-align: center;
font: 36px verdana,arial,sans-serif;
width:300px;
border:none;
color: white;
text-shadow: 0 -1px 0 rgba(0,0,0,.8);
height: 70px;
@ -100,6 +101,7 @@
text-align: left;
text-shadow: 0 1px 1px #fff;
margin: 16px auto 0;
display:block;
}
#padname{
height:38px;
@ -158,8 +160,8 @@
<div id="wrapper">
<% e.begin_block("indexWrapper"); %>
<div id="inner">
<div id="button" onclick="go2Random()" data-l10n-id="index.newPad"></div>
<div id="label" data-l10n-id="index.createOpenPad"></div>
<buttOn id="button" onclick="go2Random()" data-l10n-id="index.newPad"></button>
<label id="label" for="padname" data-l10n-id="index.createOpenPad"></label>
<form action="#" onsubmit="go2Name();return false;">
<input type="text" id="padname" autofocus x-webkit-speech>
<button type="submit">OK</button>

View File

@ -56,17 +56,17 @@
<!-- head and body had been removed intentionally -->
<% e.begin_block("body"); %>
<div id="editbar" class="toolbar">
<div id="editbar" class="toolbar" title="Toolbar (Alt F9)">
<div id="overlay">
<div id="overlay-inner"></div>
</div>
<ul class="menu_left">
<ul class="menu_left" role="toolbar">
<% e.begin_block("editbarMenuLeft"); %>
<%- toolbar.menu(settings.toolbar.left) %>
<% e.end_block(); %>
</ul>
<ul class="menu_right">
<ul class="menu_right" role="toolbar">
<% e.begin_block("editbarMenuRight"); %>
<%- toolbar.menu(settings.toolbar.right) %>
<% e.end_block(); %>
@ -88,7 +88,7 @@
<div id="myusernameform"><input type="text" id="myusernameedit" disabled="disabled" data-l10n-id="pad.userlist.entername"></div>
<div id="mystatusform"><input type="text" id="mystatusedit" disabled="disabled"></div>
</div>
<div id="otherusers">
<div id="otherusers" aria-role="document">
<div id="guestprompts"></div>
<table id="otheruserstable" cellspacing="0" cellpadding="0" border="0">
<tr><td></td></tr>
@ -160,6 +160,22 @@
<select id="viewfontmenu">
<option value="normal" data-l10n-id="pad.settings.fontType.normal"></option>
<option value="monospace" data-l10n-id="pad.settings.fontType.monospaced"></option>
<option value="opendyslexic" data-l10n-id="pad.settings.fontType.opendyslexic"></option>
<option value="comicsans" data-l10n-id="pad.settings.fontType.comicsans"></option>
<option value="georgia" data-l10n-id="pad.settings.fontType.georgia"></option>
<option value="impact" data-l10n-id="pad.settings.fontType.impact"></option>
<option value="lucida" data-l10n-id="pad.settings.fontType.lucida"></option>
<option value="lucidasans" data-l10n-id="pad.settings.fontType.lucidasans"></option>
<option value="palatino" data-l10n-id="pad.settings.fontType.palatino"></option>
<option value="tahoma" data-l10n-id="pad.settings.fontType.tahoma"></option>
<option value="timesnewroman" data-l10n-id="pad.settings.fontType.timesnewroman"></option>
<option value="trebuchet" data-l10n-id="pad.settings.fontType.trebuchet"></option>
<option value="verdana" data-l10n-id="pad.settings.fontType.verdana"></option>
<option value="symbol" data-l10n-id="pad.settings.fontType.symbol"></option>
<option value="webdings" data-l10n-id="pad.settings.fontType.webdings"></option>
<option value="wingdings" data-l10n-id="pad.settings.fontType.wingdings"></option>
<option value="sansserif" data-l10n-id="pad.settings.fontType.sansserif"></option>
<option value="serif" data-l10n-id="pad.settings.fontType.serif"></option>
</select>
</td>
</tr>
@ -306,7 +322,7 @@
<% e.end_block(); %>
</div>
<div id="chaticon" onclick="chat.show();return false;">
<div id="chaticon" onclick="chat.show();return false;" title="Chat (Alt C)">
<span id="chatlabel" data-l10n-id="pad.chat"></span>
<span class="buttonicon buttonicon-chat"></span>
<span id="chatcounter">0</span>
@ -317,7 +333,7 @@
<a id="titlecross" onClick="chat.hide();return false;">-&nbsp;</a>
<a id="titlesticky" onClick="chat.stickToScreen(true);$('#options-stickychat').prop('checked', true);return false;" title="Stick chat to screen">&nbsp;&nbsp;</a>
</div>
<div id="chattext" class="authorColors">
<div id="chattext" class="authorColors" aria-live="polite" aria-relevant="additions removals text" role="log" aria-atomic="false">
<div alt="loading.." id="chatloadmessagesball" class="chatloadmessages loadingAnimation" align="top"></div>
<button id="chatloadmessagesbutton" class="chatloadmessages" data-l10n-id="pad.chat.loadmessages"></button>
</div>

View File

@ -61,11 +61,11 @@
<div id="ui-slider-bar"></div>
</div>
<div id="playpause_button">
<div id="playpause_button_icon" class=""></div>
<button id="playpause_button_icon" class=""></button>
</div>
<div id="steppers">
<div class="stepper" id="leftstep"></div>
<div class="stepper" id="rightstep"></div>
<button class="stepper" id="leftstep"></button>
<button class="stepper" id="rightstep"></button>
</div>
</div>
@ -176,11 +176,41 @@
<% e.end_block(); %>
</div>
<div class="popup" id="settings">
<tr>
<td>
<label for="viewfontmenu" data-l10n-id="pad.settings.fontType">Font type:</label>
</td>
<td>
<select id="viewfontmenu">
<option value="normal" data-l10n-id="pad.settings.fontType.normal"></option>
<option value="monospace" data-l10n-id="pad.settings.fontType.monospaced"></option>
<option value="opendyslexic" data-l10n-id="pad.settings.fontType.opendyslexic"></option>
<option value="comicsans" data-l10n-id="pad.settings.fontType.comicsans"></option>
<option value="georgia" data-l10n-id="pad.settings.fontType.georgia"></option>
<option value="impact" data-l10n-id="pad.settings.fontType.impact"></option>
<option value="lucida" data-l10n-id="pad.settings.fontType.lucida"></option>
<option value="lucidasans" data-l10n-id="pad.settings.fontType.lucidasans"></option>
<option value="palatino" data-l10n-id="pad.settings.fontType.palatino"></option>
<option value="tahoma" data-l10n-id="pad.settings.fontType.tahoma"></option>
<option value="timesnewroman" data-l10n-id="pad.settings.fontType.timesnewroman"></option>
<option value="trebuchet" data-l10n-id="pad.settings.fontType.trebuchet"></option>
<option value="verdana" data-l10n-id="pad.settings.fontType.verdana"></option>
<option value="symbol" data-l10n-id="pad.settings.fontType.symbol"></option>
<option value="webdings" data-l10n-id="pad.settings.fontType.webdings"></option>
<option value="wingdings" data-l10n-id="pad.settings.fontType.wingdings"></option>
<option value="sansserif" data-l10n-id="pad.settings.fontType.sansserif"></option>
<option value="serif" data-l10n-id="pad.settings.fontType.serif"></option>
</select>
</td>
</tr>
</div>
<!-- export code -->
<div id="import_export">
<div id="export" class="popup">
<p data-l10n-id="timeslider.exportCurrent"></p>
<a id="exportetherpada" target="_blank" class="exportlink"><div class="exporttype" id="exportetherpad" data-l10n-id="pad.importExport.exportetherpad"></div></a>
<a id="exporthtmla" target="_blank" class="exportlink"><div class="exporttype" id="exporthtml" data-l10n-id="pad.importExport.exporthtml"></div></a>
<a id="exportplaina" target="_blank" class="exportlink"><div class="exporttype" id="exportplain" data-l10n-id="pad.importExport.exportplain"></div></a>
<a id="exportworda" target="_blank" class="exportlink"><div class="exporttype" id="exportword" data-l10n-id="pad.importExport.exportword"></div></a>

View File

@ -0,0 +1,113 @@
var assert = require('assert')
supertest = require(__dirname+'/../../../../src/node_modules/supertest'),
fs = require('fs'),
api = supertest('http://localhost:9001');
path = require('path');
var filePath = path.join(__dirname, '../../../../APIKEY.txt');
var apiKey = fs.readFileSync(filePath, {encoding: 'utf-8'});
apiKey = apiKey.replace(/\n$/, "");
var apiVersion = 1;
var authorID = "";
var padID = makeid();
var timestamp = Date.now();
describe('API Versioning', function(){
it('errors if can not connect', function(done) {
api.get('/api/')
.expect(function(res){
apiVersion = res.body.currentVersion;
if (!res.body.currentVersion) throw new Error("No version set in API");
return;
})
.expect(200, done)
});
})
// BEGIN GROUP AND AUTHOR TESTS
/////////////////////////////////////
/////////////////////////////////////
/* Tests performed
-> createPad(padID)
-> createAuthor([name]) -- should return an authorID
-> appendChatMessage(padID, text, authorID, time)
-> getChatHead(padID)
-> getChatHistory(padID)
*/
describe('createPad', function(){
it('creates a new Pad', function(done) {
api.get(endPoint('createPad')+"&padID="+padID)
.expect(function(res){
if(res.body.code !== 0) throw new Error("Unable to create new Pad");
})
.expect('Content-Type', /json/)
.expect(200, done)
});
})
describe('createAuthor', function(){
it('Creates an author with a name set', function(done) {
api.get(endPoint('createAuthor'))
.expect(function(res){
if(res.body.code !== 0 || !res.body.data.authorID) throw new Error("Unable to create author");
authorID = res.body.data.authorID; // we will be this author for the rest of the tests
})
.expect('Content-Type', /json/)
.expect(200, done)
});
})
describe('appendChatMessage', function(){
it('Adds a chat message to the pad', function(done) {
api.get(endPoint('appendChatMessage')+"&padID="+padID+"&text=blalblalbha&authorID="+authorID+"&time="+timestamp)
.expect(function(res){
if(res.body.code !== 0) throw new Error("Unable to create chat message");
})
.expect('Content-Type', /json/)
.expect(200, done)
});
})
describe('getChatHead', function(){
it('Gets the head of chat', function(done) {
api.get(endPoint('getChatHead')+"&padID="+padID)
.expect(function(res){
if(res.body.data.chatHead !== 0) throw new Error("Chat Head Length is wrong");
if(res.body.code !== 0) throw new Error("Unable to get chat head");
})
.expect('Content-Type', /json/)
.expect(200, done)
});
})
describe('getChatHistory', function(){
it('Gets Chat History of a Pad', function(done) {
api.get(endPoint('getChatHistory')+"&padID="+padID)
.expect(function(res){
if(res.body.data.messages.length !== 1) throw new Error("Chat History Length is wrong");
if(res.body.code !== 0) throw new Error("Unable to get chat history");
})
.expect('Content-Type', /json/)
.expect(200, done)
});
})
var endPoint = function(point){
return '/api/'+apiVersion+'/'+point+'?apikey='+apiKey;
}
function makeid()
{
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for( var i=0; i < 5; i++ ){
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}

View File

@ -47,6 +47,11 @@ describe("clear authorship colors button", function(){
var hasAuthorClass = inner$("div").first().attr("class").indexOf("author") !== -1;
expect(hasAuthorClass).to.be(false);
setTimeout(function(){
var disconnectVisible = chrome$("div.disconnected").attr("class").indexOf("visible") === -1
expect(disconnectVisible).to.be(true);
},1000);
done();
});

View File

@ -24,7 +24,7 @@ describe("font select", function(){
//check if font changed to monospace
var fontFamily = inner$("body").css("font-family").toLowerCase();
expect(fontFamily).to.be("monospace");
expect(fontFamily).to.be("courier new");
done();
});