Merge branch 'develop' of github.com:ether/etherpad-lite into develop

Conflicts:
	README.md
This commit is contained in:
John McLear 2012-11-21 09:10:54 -08:00
commit 8d0d71eddb
66 changed files with 11576 additions and 287 deletions

22
.travis.yml Normal file
View file

@ -0,0 +1,22 @@
language: node_js
node_js:
- "0.8"
install:
- "bin/installDeps.sh"
- "export GIT_HASH=$(cat .git/HEAD | head -c 7)"
before_script:
- "tests/frontend/travis/sauce_tunnel.sh"
script:
- "tests/frontend/travis/runner.sh"
env:
global:
- secure: "OxZ2s724S96xu02746LUN+4lBckAe1BOICJjfA4jnFPNpiNU6XoMH52f+LgG\nZzAwu6xMTv+NsaLGp6Avm3cx4GZ+jIiHe4NB9XOgYPa0r0TBIi3ueWYPDyVv\nCniS/4qX68DoFNV4lh7zMBXn0IIPxT4Wppm3desBpjWDP/SdoRs="
- SAUCE_USER=pita
jdk:
- oraclejdk6
notifications:
email:
- petermartischka@googlemail.com
irc:
channels:
- "irc.freenode.org#etherpad-lite-dev"

View file

@ -1,3 +1,34 @@
# v1.2
* Internationalization / Language / Translation support (i18n) with support for German/French
* A frontend/client side testing framework and backend build tests
* Customizable robots.txt
* Customizable app title (finally you can name your epl instance!)
* eejs render arguments are now passed on to eejs hooks through the newly introduced `renderContext` argument.
* Plugin-specific settings in settings.json (finally allowing for things like a google analytics plugin)
* Serve admin dashboard at /admin (still very limited, though)
* Modify your settings.json through the newly created UI at /admin/settings
* Fix: Import <ol>'s as <ol>'s and not as <ul>'s!
* Added solaris compatibility (bin/installDeps.sh was broken on solaris)
* Fix a bug with IE9 and Password Protected Pads using HTTPS
# v1.1.5
* We updated to express v3 (please [make sure](https://github.com/visionmedia/express/wiki/Migrating-from-2.x-to-3.x) your plugin works under express v3)
* `userColor` URL parameter which sets the initial author color
* Hooks for "padCreate", "padRemove", "padUpdate" and "padLoad" events
* Security patches concerning the handling of messages originating from clients
* Our database abstraction layer now natively supports couchDB, levelDB, mongoDB, postgres, and redis!
* We now provide a script helping you to migrate from dirtyDB to MySQL
* Support running Etherpad Lite behind IIS, using [iisnode](https://github.com/tjanczuk/iisnode/wiki)
* LibreJS Licensing information in headers of HTML templates
* Default port number to PORT env var, if port isn't specified in settings
* Fix for `convert.js`
* Raise upper char limit in chat to 999 characters
* Fixes for mobile layout
* Fixes for usage behind reverse proxy
* Improved documentation
* Fixed some opera style bugs
* Update npm and fix some bugs, this introduces
# v1.1
* Introduced Plugin framework
* Many bugfixes

View file

@ -1,5 +1,5 @@
# Developer Guidelines
(Please talk to people on the mailing list before you change this page, see our section on [how to get in touch](https://github.com/Pita/etherpad-lite#get-in-touch))
(Please talk to people on the mailing list before you change this page, see our section on [how to get in touch](https://github.com/ether/etherpad-lite#get-in-touch))
**Our goal is to iterate in small steps. Release often, release early. Evolution instead of a revolution**

133
README.md
View file

@ -1,133 +0,0 @@
<<<<<<< HEAD
# Making collaborative editing the standard on the web
# About
Etherpad lite is a really-real time collaborative editor spawned from the Hell fire of Etherpad.
We're reusing the well tested Etherpad easysync library to make it really realtime. Etherpad Lite
is based on node.js ergo is much lighter and more stable than the original Etherpad. Our hope
is that this will encourage more users to use and install a realtime collaborative editor. A smaller, manageable and well
documented codebase makes it easier for developers to improve the code and contribute towards the project.
**Etherpad vs Etherpad lite**
<table>
<tr>
<td>&nbsp;</td><td><b>Etherpad</b></td><td><b>Etherpad Lite</b></td>
</tr>
<tr>
<td align="right">Size of the folder (without git history)</td><td>30 MB</td><td>1.5 MB</td>
</tr>
<tr>
<td align="right">Languages used server side</td><td>Javascript (Rhino), Java, Scala</td><td>Javascript (node.js)</td>
</tr>
<tr>
<td align="right">Lines of server side Javascript code</td><td>~101k</td><td>~9k</td>
</tr>
<tr>
<td align="right">RAM Usage immediately after start</td><td>257 MB (grows to ~1GB)</td><td>16 MB (grows to ~30MB)</td>
</tr>
</table>
Etherpad Lite is designed to be easily embeddable and provides a [HTTP API](https://github.com/Pita/etherpad-lite/wiki/HTTP-API)
that allows your web application to manage pads, users and groups. It is recommended to use the client implementations available for this API, listed on [this wiki page](https://github.com/Pita/etherpad-lite/wiki/HTTP-API-client-libraries).
There is also a [jQuery plugin](https://github.com/johnyma22/etherpad-lite-jquery-plugin) that helps you to embed Pads into your website
**Visit [beta.etherpad.org](http://beta.etherpad.org) to test it live**
Also, check out the **[FAQ](https://github.com/Pita/etherpad-lite/wiki/FAQ)**, really!
# Installation
## Windows
### Prebuilt windows package
This package works out of the box on any windows machine, but it's not very useful for developing purposes...
1. Download the windows package <https://github.com/Pita/etherpad-lite/downloads>
2. Extract the folder
Now, run `start.bat` and open <http://localhost:9001> in your browser. You like it? [Next steps](#next-steps).
### Fancy install
You'll need [node.js](http://nodejs.org) and (optionally, though recommended) git.
1. Grab the source, either
- download <https://github.com/Pita/etherpad-lite/zipball/master>
- or `git clone https://github.com/Pita/etherpad-lite.git` (for this you need git, obviously)
2. start `bin\installOnWindows.bat`
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.
[Next steps](#next-steps).
## Linux
You'll need gzip, git, curl, libssl develop libraries, python and gcc.
*For Debian/Ubuntu*: `apt-get install gzip git-core curl python libssl-dev pkg-config build-essential`
*For Fedora/CentOS*: `yum install gzip git-core curl python openssl-devel && yum groupinstall "Development Tools"`
Additionally, you'll need [node.js](http://nodejs.org).
**As any user (we recommend creating a separate user called etherpad-lite):**
1. Move to a folder where you want to install Etherpad Lite. Clone the git repository `git clone git://github.com/Pita/etherpad-lite.git`
2. Change into the new directory containing the cloned source code `cd etherpad-lite`
Now, run `bin/run.sh` and open <http://127.0.0.1:9001> in your browser.
Update to the latest version with `git pull origin`. The next start with bin/run.sh will update the dependencies.
You like it? [Next steps](#next-steps).
# Next Steps
## Tweak the settings
You can modify the settings in `settings.json`. (If you need to handle multiple settings files, you can pass the path to a settings file to `bin/run.sh` using the `-s|--settings` option. This allows you to run multiple Etherpad Lite instances from the same installation.)
You should use a dedicated database such as "mysql", if you are planning on using etherpad-lite in a production environment, since the "dirtyDB" database driver is only for testing and/or development purposes.
## Helpful resources
The [wiki](https://github.com/Pita/etherpad-lite/wiki) is your one-stop resource for Tutorials and How-to's, really check it out! Also, feel free to improve these wiki pages.
Documentation can be found in `docs/`.
# Development
## Things you should know
Read this [git guide](http://learn.github.com/p/intro.html) and watch this [video on getting started with Etherpad Lite Development](http://youtu.be/67-Q26YH97E).
If you're new to node.js, start with Ryan Dahl's [Introduction to Node.js](http://youtu.be/jo_B4LTHi3I).
You can debug Etherpad lite using `bin/debugRun.sh`.
If you want to find out how Etherpad's `Easysync` works (the library that makes it really realtime), start with this [PDF](https://github.com/Pita/etherpad-lite/raw/master/doc/easysync/easysync-full-description.pdf) (complex, but worth reading).
## Getting started
You know all this and just want to know how you can help?
Look at the [TODO list](https://github.com/Pita/etherpad-lite/wiki/TODO) and our [Issue tracker](https://github.com/Pita/etherpad-lite/issues). (Please consider using [jshint](http://www.jshint.com/about/), if you plan to contribute code.)
Also, and most importantly, read our [**Developer Guidelines**](https://github.com/Pita/etherpad-lite/wiki/Developer-Guidelines), really!
# Get in touch
Join the [mailinglist](http://groups.google.com/group/etherpad-lite-dev) and make some noise on our freenode irc channel [#etherpad-lite-dev](http://webchat.freenode.net?channels=#etherpad-lite-dev)!
# Modules created for this project
* [ueberDB](https://github.com/Pita/ueberDB) "transforms every database into a object key value store" - manages all database access
* [channels](https://github.com/Pita/channels) "Event channels in node.js" - ensures that ueberDB operations are atomic and in series for each key
* [async-stacktrace](https://github.com/Pita/async-stacktrace) "Improves node.js stacktraces and makes it easier to handle errors"
# Donate!
* [Flattr] (http://flattr.com/thing/71378/Etherpad-Foundation)
* Paypal - Press the donate button on [etherpad.org](http://etherpad.org)
# License
[Apache License v2](http://www.apache.org/licenses/LICENSE-2.0.html)
=======
We moved!
=============
You can now find Etherpad Lite under [github.com/ether/etherpad-lite](https://github.com/ether/etherpad-lite)
>>>>>>> a5c4fb154f7414cd5dca9e50c3f2ad00e42672f5

View file

@ -52,6 +52,13 @@ echo "download windows node..."
cd bin
wget "http://nodejs.org/dist/v$NODE_VERSION/node.exe" -O ../node.exe
echo "remove git history to reduce folder size"
rm -rf .git/objects
echo "remove windows jsdom-nocontextify/test folder"
rm -rf /tmp/etherpad-lite-win/node_modules/ep_etherpad-lite/node_modules/jsdom-nocontextifiy/test/
rm -rf /tmp/etherpad-lite-win/src/node_modules/jsdom-nocontextifiy/test/
echo "create the zip..."
cd /tmp
zip -9 -r etherpad-lite-win.zip etherpad-lite-win

View file

@ -69,7 +69,7 @@ echo "Ensure that all dependencies are up to date..."
cd node_modules
[ -e ep_etherpad-lite ] || ln -s ../src ep_etherpad-lite
cd ep_etherpad-lite
npm install
npm install --loglevel warn
) || {
rm -rf node_modules
exit 1
@ -79,7 +79,7 @@ echo "Ensure jQuery is downloaded and up to date..."
DOWNLOAD_JQUERY="true"
NEEDED_VERSION="1.7.1"
if [ -f "src/static/js/jquery.js" ]; then
if [ $(uname) = "SunOS"]; then
if [ $(uname) = "SunOS" ]; then
VERSION=$(cat src/static/js/jquery.js | head -n 3 | ggrep -o "v[0-9]\.[0-9]\(\.[0-9]\)\?");
else
VERSION=$(cat src/static/js/jquery.js | head -n 3 | grep -o "v[0-9]\.[0-9]\(\.[0-9]\)\?");

View file

@ -13,7 +13,7 @@ cmd /C node -e %check_version% || exit /B 1
echo _
echo Installing etherpad-lite and dependencies...
cmd /C npm install src/ || exit /B 1
cmd /C npm install src/ --loglevel warn || exit /B 1
echo _
echo Copying custom templates...

View file

@ -1,11 +1,17 @@
var dirty = require("../src/node_modules/ueberDB/node_modules/dirty")('var/dirty.db');
var db = require("../src/node/db/DB");
require("ep_etherpad-lite/node_modules/npm").load({}, function(er,npm) {
db.init(function() {
db = db.db;
dirty.on("load", function() {
dirty.forEach(function(key, value) {
db.set(key, value);
process.chdir(npm.root+'/..')
var dirty = require("ep_etherpad-lite/node_modules/ueberDB/node_modules/dirty")('var/dirty.db');
var db = require("ep_etherpad-lite/node/db/DB");
db.init(function() {
db = db.db;
dirty.on("load", function() {
dirty.forEach(function(key, value) {
db.set(key, value);
});
});
});
});

View file

@ -52,3 +52,11 @@ Default: false
* Boolean
Default: false
## lang
* String
Default: en
Example: `lang=ar` (translates the interface into Arabic)

View file

@ -1,5 +1,6 @@
@include documentation
@include cusotm_static
@include localization
@include custom_static
@include api/api
@include plugins
@include database

19
doc/localization.md Normal file
View file

@ -0,0 +1,19 @@
# Localization
Etherpad lite provides a multi-language user interface, that's apart from your users' content, so users from different countries can collaborate on a single document, while still having the user interface displayed in their mother tongue.
## Translating
`/src/locales` contains files for all supported languages which contain the translated strings. To add support for a new language, copy the English language file named `en.ini` and translate it.
Translation files are simply `*.ini` files and look like this:
```
pad.modals.connected = Connecté.
pad.modals.uderdup = Ouvrir dans une nouvelle fenêtre.
pad.toolbar.unindent.title = Désindenter
pad.toolbar.undo.title = Annuler (Ctrl-Z)
timeslider.pageTitle = {{appTitle}} Curseur temporel
```
There must be only one translation per line. Each translation consists of a key (the id of the string that is to be translated), an equal sign and the translated string. Anything after the equa sign will be used as the translated string (you may put some spaces after `=` for better readability, though). Terms in curly braces must not be touched but left as they are, since they represent a dynamically changing part of the string like a variable. Imagine a message welcoming a user: `Welcome, {{userName}}!` would be translated as `Ahoy, {{userName}}!` in pirate.
## Under the hood
We use a `language` cookie to save your language settings if you change them. If you don't, we autodetect your locale using information from your browser. Now, that we know your preferred language this information is feeded into a very nice library called [webL10n](https://github.com/fabi1cazenave/webL10n), which loads the appropriate translations and applies them to our templates, providing translation params, pluralization, include rules and even a nice javascript API along the way.

View file

@ -5,6 +5,7 @@
"restartServer": "ep_etherpad-lite/node/hooks/express:restartServer"
} },
{ "name": "static", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/static:expressCreateServer" } },
{ "name": "i18n", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/i18n:expressCreateServer" } },
{ "name": "specialpages", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/specialpages:expressCreateServer" } },
{ "name": "padurlsanitize", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/padurlsanitize:expressCreateServer" } },
{ "name": "padreadonly", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/padreadonly:expressCreateServer" } },
@ -13,6 +14,7 @@
{ "name": "importexport", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/importexport:expressCreateServer" } },
{ "name": "errorhandling", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/errorhandling:expressCreateServer" } },
{ "name": "socketio", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/socketio:expressCreateServer" } },
{ "name": "tests", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/tests:expressCreateServer" } },
{ "name": "admin", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/admin:expressCreateServer" } },
{ "name": "adminplugins", "hooks": {
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/adminplugins:expressCreateServer",

32
src/locales/bn.ini Normal file
View file

@ -0,0 +1,32 @@
; Exported from translatewiki.net
; Author: Nasir8891
[bn]
index.newPad = নতুন প্যাড
index.createOpenPad = অথবা নাম লিখে প্যাড খুলুন/তৈরী করুন:
pad.toolbar.bold.title = গাড় করা (Ctrl-B)
pad.toolbar.italic.title = বাঁকা করা (Ctrl-I)
pad.toolbar.settings.title = সেটিং
pad.colorpicker.save = সংরক্ষণ
pad.colorpicker.cancel = বাতিল
pad.loading = লোডিং...
pad.settings.language = ভাষা:
pad.importExport.successful = সফল!
; Fuzzy
pad.importExport.export = এই প্যাডটি এক্সপোর্ট করুন
pad.importExport.exporthtml = এইচটিএমএল
pad.importExport.exportplain = সাধারণ লেখা
pad.importExport.exportword = মাইক্রোসফট ওয়ার্ড
pad.importExport.exportpdf = পিডিএফ
pad.importExport.exportopen = ওডিএফ (ওপেন ডকুমেন্ট ফরম্যাট)
pad.modals.deleted = অপসারিত।
pad.modals.deleted.explanation = এই প্যাডটি অপসারণ করা হয়েছে।
pad.modals.disconnected.explanation = সার্ভারের সাথে যোগাযোগ করা যাচ্ছে না
pad.share = শেয়ার করুন
pad.share.link = লিংক
pad.share.emebdcode = ইউআরএল সংযোজন
pad.chat = চ্যাট
pad.chat.title = এই প্যাডের জন্য চ্যাট চালু করুন।
timeslider.toolbar.returnbutton = প্যাডে ফিরে যাও
timeslider.toolbar.authors = লেখকগণ:
timeslider.toolbar.authorsList = কোনো লেখক নেই
timeslider.exportCurrent = বর্তমান সংস্করণটি এক্সপোর্ট করুন:

78
src/locales/de.ini Normal file
View file

@ -0,0 +1,78 @@
; Exported from translatewiki.net
[de]
index.newPad = Neues Pad
index.createOpenPad = Pad mit folgendem Namen öffnen
pad.toolbar.bold.title = Fett (Strg-B)
pad.toolbar.italic.title = Kursiv (Strg-I)
pad.toolbar.underline.title = Unterstrichen (Strg-U)
pad.toolbar.strikethrough.title = Durchgestrichen
pad.toolbar.ol.title = Nummerierte Liste
pad.toolbar.ul.title = Ungeordnete Liste
pad.toolbar.indent.title = Einrücken
pad.toolbar.unindent.title = Ausrücken
pad.toolbar.undo.title = Rückgängig (Strg-Z)
pad.toolbar.redo.title = Wiederholen (Strg-Y)
pad.toolbar.clearAuthorship.title = Autorenfarben zurücksetzen
pad.toolbar.import_export.title = Import/Export von verschiedenen Dateiformaten
pad.toolbar.timeslider.title = Pad-Geschichte anzeigen
pad.toolbar.savedRevision.title = Diese Revision markieren
pad.toolbar.settings.title = Einstellungen
pad.toolbar.embed.title = Dieses Pad teilen oder einbetten
pad.toolbar.showusers.title = Verbundene Benutzer anzeigen
pad.colorpicker.save = Speichern
pad.colorpicker.cancel = Abbrechen
pad.loading = Laden...
pad.settings.padSettings = Pad Einstellungen
pad.settings.myView = Eigene Ansicht
pad.settings.stickychat = Chat immer anzeigen
pad.settings.colorcheck = Autorenfarben anzeigen
pad.settings.linenocheck = Zeilennummern
pad.settings.fontType = Schriftart:
pad.settings.fontType.normal = Normal
pad.settings.fontType.monospaced = Monospace
pad.settings.globalView = Gemeinsame Ansicht
pad.settings.language = Sprache:
pad.importExport.import_export = Import/Export
pad.importExport.import = Datei oder Dokument hochladen
pad.importExport.successful = Erfolgreich!
; Fuzzy
pad.importExport.export = Dieses Pad exportieren
pad.importExport.exporthtml = HTML
pad.importExport.exportplain = Reiner Text
pad.importExport.exportword = Microsoft Word
pad.importExport.exportpdf = PDf
pad.importExport.exportopen = ODF (Open Document Format)
pad.importExport.exportdokuwiki = DokuWiki
pad.modals.connected = Verbunden.
pad.modals.reconnecting = Wiederherstellen der Verbindung...
pad.modals.forcereconnect = Erneut Verbinden
pad.modals.uderdup = In einem anderen Fenster geöffnet
pad.modals.userdup.explanation = Dieses Pad scheint in mehr als einem Browser-Fenster auf diesem Computer geöffnet zu sein.
pad.modals.userdup.advice = Um dieses Fenster zu benutzen, verbinden Sie bitte erneut.
pad.modals.unauth = Nicht Authorisiert.
pad.modals.unauth.explanation = Ihre Befugnisse auf dieses Pad zuzugreifen haben sich geädert. Versuchen Sie, erneut zu verbinden.
pad.modals.looping = Verbindung unterbrochen.
pad.modals.looping.explanation = Es gibt Probleme bei der Kommunikation mit dem Synchronisationsserver.
pad.modals.looping.cause = Möglicherweise verläuft Ihre Verbindung durch eine inkompatible Firewall oder einen inkompatiblen Proxy.
pad.modals.initsocketfail = Server nicht erreichbar.
pad.modals.initsocketfail.explanation = Es konnte keine Verbindung zum Synchronisationsserver hergestellt werden.
pad.modals.initsocketfail.cause = Dies könnte an Ihrem Browser oder Ihrer Internet-Verbindung liegen.
pad.modals.slowcommit = Verbindung unterbrochen.
pad.modals.slowcommit.explanation = Der Server reagiert nicht.
pad.modals.slowcommit.cause = Dies könnte an Problemen mit Netzwerk-Konnektivität liegen. Möglicherweise ist der Server aber auch überlastet.
pad.modals.deleted = Entfernt.
pad.modals.deleted.explanation = Dieses Pad wurde entfernt.
pad.modals.disconnected = Verbindung unterbrochen.
pad.modals.disconnected.explanation = Die Verbindung zum Synchronisationsserver wurde unterbrochen.
pad.modals.disconnected.cause = Möglicherweise ist der Server nicht erreichbar. Bitte benachrichtigen Sie uns, falls dies weiterhin passiert.
pad.share = Dieses Pad teilen
pad.share.readonly = Eingeschränkter zugriff (Nur lesen)
pad.share.link = Link
pad.share.emebdcode = In Webseite einbetten
pad.chat = Chat
pad.chat.title = Den Chat für dieses Pad öffnen
timeslider.pageTitle = {{appTitle}} Pad-Geschichte
timeslider.toolbar.returnbutton = Zurück zum Pad
timeslider.toolbar.authors = Autoren:
timeslider.toolbar.authorsList = keine Autoren
timeslider.exportCurrent = Exportiere diese Version als:

77
src/locales/en.ini Normal file
View file

@ -0,0 +1,77 @@
[*]
index.newPad = New Pad
index.createOpenPad = or create/open a Pad with the name:
pad.toolbar.bold.title = Bold (Ctrl-B)
pad.toolbar.italic.title = Italic (Ctrl-I)
pad.toolbar.underline.title = Underline (Ctrl-U)
pad.toolbar.strikethrough.title = Strikethrough
pad.toolbar.ol.title = Ordered list
pad.toolbar.ul.title = Unordered List
pad.toolbar.indent.title = Indent
pad.toolbar.unindent.title = Outdent
pad.toolbar.undo.title = Undo (Ctrl-Z)
pad.toolbar.redo.title = Redo (Ctrl-Y)
pad.toolbar.clearAuthorship.title = Clear Authorship Colors
pad.toolbar.import_export.title = Import/Export from/to different file formats
pad.toolbar.timeslider.title = Timeslider
pad.toolbar.savedRevision.title = Saved Revisions
pad.toolbar.settings.title = Settings
pad.toolbar.embed.title = Embed this pad
pad.toolbar.showusers.title = Show the users on this pad
pad.colorpicker.save = Save
pad.colorpicker.cancel = Cancel
pad.loading = Loading...
pad.settings.padSettings = Pad Settings
pad.settings.myView = My View
pad.settings.stickychat = Chat always on screen
pad.settings.colorcheck = Authorship colors
pad.settings.linenocheck = Line numbers
pad.settings.fontType = Font type:
pad.settings.fontType.normal = Normal
pad.settings.fontType.monospaced = Monospace
pad.settings.globalView = Global View
pad.settings.language = Language:
pad.importExport.import_export = Import/Export
pad.importExport.import = Upload any text file or document
pad.importExport.successful = Successful!
pad.importExport.export = Export current pad as:
pad.importExport.exporthtml = HTML
pad.importExport.exportplain = Plain text
pad.importExport.exportword = Microsoft Word
pad.importExport.exportpdf = PDF
pad.importExport.exportopen = ODF (Open Document Format)
pad.importExport.exportdokuwiki = DokuWiki
pad.modals.connected = Connected.
pad.modals.reconnecting = Reconnecting to your pad..
pad.modals.forcereconnect = Force reconnect
pad.modals.uderdup = Opened in another window
pad.modals.userdup.explanation = This pad seems to be opened in more than one browser window on this computer.
pad.modals.userdup.advice = Reconnect to use this window instead.
pad.modals.unauth = Not authorized
pad.modals.unauth.explanation = Your permissions have changed while viewing this page. Try to reconnect.
pad.modals.looping = Disconnected.
pad.modals.looping.explanation = There are communication problems with the synchronization server.
pad.modals.looping.cause = Perhaps you connected through an incompatible firewall or proxy.
pad.modals.initsocketfail = Server is unreachable.
pad.modals.initsocketfail.explanation = Couldn't connect to the synchronization server.
pad.modals.initsocketfail.cause = This is probably due to a problem with your browser or your internet connection.
pad.modals.slowcommit = Disconnected.
pad.modals.slowcommit.explanation = The server is not responding.
pad.modals.slowcommit.cause = This could be due to problems with network connectivity.
pad.modals.deleted = Deleted.
pad.modals.deleted.explanation = This pad has been removed.
pad.modals.disconnected = You have been disconnected.
pad.modals.disconnected.explanation = The connection to the server was lost
pad.modals.disconnected.cause = The server may be unavailable. Please notify us if this continues to happen.
pad.share = Share this pad
pad.share.readonly = Read only
pad.share.link = Link
pad.share.emebdcode = Embed URL
pad.chat = Chat
pad.chat.title = Open the chat for this pad.
timeslider.pageTitle = {{appTitle}} Timeslider
timeslider.toolbar.returnbutton = Return to pad
timeslider.toolbar.authors = Authors:
timeslider.toolbar.authorsList = No Authors
timeslider.exportCurrent = Export current version as:

78
src/locales/es.ini Normal file
View file

@ -0,0 +1,78 @@
; Exported from translatewiki.net
[es]
index.newPad = Nuevo Pad
index.createOpenPad = o puedes crear/abrir un Pad con el nombre:
pad.toolbar.bold.title = Negrita (Ctrl-B)
pad.toolbar.italic.title = Cursiva (Ctrl-I)
pad.toolbar.underline.title = Subrayado (Ctrl-U)
pad.toolbar.strikethrough.title = Tachado
pad.toolbar.ol.title = Lista ordenada
pad.toolbar.ul.title = Lista desordenada
pad.toolbar.indent.title = Sangrar
pad.toolbar.unindent.title = Desangrar
pad.toolbar.undo.title = Deshacer (Ctrl-Z)
pad.toolbar.redo.title = Rehacer (Ctrl-Y)
pad.toolbar.clearAuthorship.title = Eliminar los colores de los autores
pad.toolbar.import_export.title = Importar/Exportar a diferentes formatos de archivos
pad.toolbar.timeslider.title = Línea de tiempo
pad.toolbar.savedRevision.title = Revisiones guardadas
pad.toolbar.settings.title = Configuración
pad.toolbar.embed.title = Incrustar este pad
pad.toolbar.showusers.title = Mostrar los usuarios de este pad
pad.colorpicker.save = Guardar
pad.colorpicker.cancel = Cancelar
pad.loading = Cargando...
pad.settings.padSettings = Configuración del Pad
pad.settings.myView = Mi vista
pad.settings.stickychat = Chat siempre encima
pad.settings.colorcheck = Color de autoría
pad.settings.linenocheck = Números de línea
pad.settings.fontType = Tipografía:
pad.settings.fontType.normal = Normal
pad.settings.fontType.monospaced = Monoespacio
pad.settings.globalView = Vista global
pad.settings.language = Idioma:
pad.importExport.import_export = Importar/Exportar
pad.importExport.import = Subir cualquier texto o documento
pad.importExport.successful = ¡Operación exitosa!
; Fuzzy
pad.importExport.export = Exporta el pad actual como
pad.importExport.exporthtml = HTML
pad.importExport.exportplain = Texto plano
pad.importExport.exportword = Microsoft Word
pad.importExport.exportpdf = PDF
pad.importExport.exportopen = ODF (Open Document Format)
pad.importExport.exportdokuwiki = DokuWiki
pad.modals.connected = Conectado.
pad.modals.reconnecting = Reconectando a tu pad..
pad.modals.forcereconnect = Reconexión forzosa
pad.modals.uderdup = Abrir en otra ventana
pad.modals.userdup.explanation = Este pad parece estar abierto en más de una ventana de tu navegador.
pad.modals.userdup.advice = Reconectar para usar esta ventana.
pad.modals.unauth = No autorizado.
pad.modals.unauth.explanation = Los permisos han cambiado mientras estabas viendo esta página. Intenta reconectar de nuevo.
pad.modals.looping = Desconectado.
pad.modals.looping.explanation = Estamos teniendo problemas con la sincronización en el servidor.
pad.modals.looping.cause = Quizás su conexión fluya a través de un proxy o un cortafuegos incompatible.
pad.modals.initsocketfail = Servidor incalcanzable.
pad.modals.initsocketfail.explanation = No se pudo conectar al sevidor de sincronización.
pad.modals.initsocketfail.cause = Puede ser a causa de tu navegador o de una caída en tu conexión de Internet.
pad.modals.slowcommit = Desconectado.
pad.modals.slowcommit.explanation = El servidor no responde.
pad.modals.slowcommit.cause = Puede deberse a problemas con tu conexión de red.
pad.modals.deleted = Borrado.
pad.modals.deleted.explanation = Este pad ha sido borrado.
pad.modals.disconnected = Has sido desconectado.
pad.modals.disconnected.explanation = Se perdió la conexión con el servidor
pad.modals.disconnected.cause = El servidor podría no estar disponible. Contacte con nosotros si esto continúa sucediendo.
pad.share = Compatir el pad
pad.share.readonly = Sólo lectura
pad.share.link = Enlace
pad.share.emebdcode = Incrustar URL
pad.chat = Chat
pad.chat.title = Abrir el chat para este pad.
timeslider.pageTitle = {{appTitle}} Línea de tiempo
timeslider.toolbar.returnbutton = Volver al pad
timeslider.toolbar.authors = Autores:
timeslider.toolbar.authorsList = Sin autores
timeslider.exportCurrent = Exportar la versión actual como:

49
src/locales/fi.ini Normal file
View file

@ -0,0 +1,49 @@
; Exported from translatewiki.net
; Author: Nike
[fi]
index.newPad = Uusi muistio
index.createOpenPad = tai avaa muistio nimellä:
pad.toolbar.bold.title = Lihavointi (Ctrl-B)
pad.toolbar.italic.title = Kursivointi (Ctrl-I)
pad.toolbar.underline.title = Alleviivaus (Ctrl-U)
pad.toolbar.strikethrough.title = Yliviivaus
pad.toolbar.ol.title = Numeroitu lista
pad.toolbar.ul.title = Numeroimaton lista
pad.toolbar.indent.title = Sisennä
pad.toolbar.unindent.title = Ulonna
pad.toolbar.undo.title = Kumoa (Ctrl-Z)
pad.toolbar.redo.title = Tee uudelleen (Ctrl-Y)
pad.toolbar.clearAuthorship.title = Poista kirjoittavärit
pad.toolbar.import_export.title = Tuo tai vie eri muotoihin
pad.toolbar.savedRevision.title = Tallennetut versiot
pad.toolbar.settings.title = Asetukset
pad.toolbar.embed.title = Upota muistio
pad.toolbar.showusers.title = Näytä muistion käyttäjät
pad.colorpicker.save = Tallenna
pad.colorpicker.cancel = Peru
pad.loading = Ladataan…
pad.settings.padSettings = Muistion asetukset
pad.settings.myView = Oma näkymä
pad.settings.stickychat = Keskustelu aina näkyvissä
pad.settings.colorcheck = Kirjoittavärit
pad.settings.linenocheck = Rivinumerot
pad.settings.fontType = Kirjasintyyppi:
pad.settings.fontType.normal = normaali
pad.settings.fontType.monospaced = tasalevyinen
pad.settings.language = Kieli:
pad.importExport.import_export = Tuonti/vienti
pad.importExport.exporthtml = HTML
pad.importExport.exportplain = Muotoilematon teksti
pad.importExport.exportword = Microsoft Word
pad.importExport.exportpdf = PDF
pad.importExport.exportopen = ODF (Open Document Format)
pad.importExport.exportdokuwiki = DokuWiki
pad.modals.connected = Yhdistetty.
pad.modals.reconnecting = Herätellään yhteyttä muistioon...
pad.modals.forcereconnect = Pakota uudelleenyhdistäminen
pad.share = Jaa muistio
pad.share.readonly = Vain luku
pad.share.link = Linkki
pad.share.emebdcode = Upotusosoite
pad.chat = Keskustelu
timeslider.toolbar.returnbutton = Palaa muistioon

78
src/locales/fr.ini Normal file
View file

@ -0,0 +1,78 @@
; Exported from translatewiki.net
[fr]
index.newPad = Nouveau Pad
index.createOpenPad = ou créer/ouvrir un Pad intitulé
pad.toolbar.bold.title = Gras (Ctrl-B)
pad.toolbar.italic.title = Italique (Ctrl-I)
pad.toolbar.underline.title = Souligner (Ctrl-U)
pad.toolbar.strikethrough.title = Barrer
pad.toolbar.ol.title = Liste ordonnée
pad.toolbar.ul.title = Liste non-ordonnée
pad.toolbar.indent.title = Indenter
pad.toolbar.unindent.title = Désindenter
pad.toolbar.undo.title = Annuler (Ctrl-Z)
pad.toolbar.redo.title = Rétablir (Ctrl-Y)
pad.toolbar.clearAuthorship.title = Effacer les couleurs identifiant les auteurs
pad.toolbar.import_export.title = Importer/Exporter de/vers un format de fichier différent
pad.toolbar.timeslider.title = Navigateur d'historique
pad.toolbar.savedRevision.title = Versions enregistrées
pad.toolbar.settings.title = Paramètres
pad.toolbar.embed.title = Intégrer ce Pad
pad.toolbar.showusers.title = Afficher les utilisateurs du Pad
pad.colorpicker.save = Sauver
pad.colorpicker.cancel = Annuler
pad.loading = Chargement...
pad.settings.padSettings = Paramètres du Pad
pad.settings.myView = Ma vue
pad.settings.stickychat = Messagerie toujours affichée
pad.settings.colorcheck = Couleurs d'identification
pad.settings.linenocheck = Numéros des lignes
pad.settings.fontType = Type de police:
pad.settings.fontType.normal = Normal
pad.settings.fontType.monospaced = Monospace
pad.settings.globalView = Vue d'ensemble
pad.settings.language = Langue:
pad.importExport.import_export = Importer/Exporter
pad.importExport.import = Charger un texte ou un document
pad.importExport.successful = Traitement effectué!
; Fuzzy
pad.importExport.export = Exporter ce Pad vers
pad.importExport.exporthtml = HTML
pad.importExport.exportplain = Texte brut
pad.importExport.exportword = Microsoft Word
pad.importExport.exportpdf = PDf
pad.importExport.exportopen = ODF (Open Document Format)
pad.importExport.exportdokuwiki = DokuWiki
pad.modals.connected = Connecté.
pad.modals.reconnecting = Reconnexion vers votre Pad...
pad.modals.forcereconnect = Forcer la reconnexion.
pad.modals.uderdup = Ouvrir dans une nouvelle fenêtre
pad.modals.userdup.explanation = Ce Pad semble avoir été ouvert dans plusieurs fenêtres de votre fureteur sur cet ordinateur.
pad.modals.userdup.advice = Se reconnecter en utilisant cette fenêtre.
pad.modals.unauth = Not authorized Non authorisé
pad.modals.unauth.explanation = Vos permissions ont été changées lors de la visualisation de cette page. Essayer de vous reconnecter.
pad.modals.looping = Disconnected. Déconnecté.
pad.modals.looping.explanation = Nous éprouvons un problème de communication au serveur de synchronisation.
pad.modals.looping.cause = Il est possible que leur connection soit protégée par un pare-feu incompatible ou un serveur proxy incompatible.
pad.modals.initsocketfail = Le serveur est introuvable.
pad.modals.initsocketfail.explanation = Impossible de se connecter au serveur de synchronisation.
pad.modals.initsocketfail.cause = La cause de ce problème peut être liée à votre fureteur web.
pad.modals.slowcommit = Disconnected. Déconnecté
pad.modals.slowcommit.explanation = Le serveur ne répond pas.
pad.modals.slowcommit.cause = La cause de ce problème peut être liée à une erreur de connectivité du réseau.
pad.modals.deleted = Supprimé.
pad.modals.deleted.explanation = Ce Pad a été supprimé.
pad.modals.disconnected = Vous avez été déconnecté.
pad.modals.disconnected.explanation = La connexion au serveur a échoué.
pad.modals.disconnected.cause = Ce serveur est possiblement hors-ligne. Veuillez nous joindre si le problème persiste.
pad.share = Partager ce Pad
pad.share.readonly = Lecture seule
pad.share.link = Lien
pad.share.emebdcode = Lien à intégrer
pad.chat = Messagerie
pad.chat.title = Ouvrir la messagerie liée au Pad.
timeslider.pageTitle = {{appTitle}} Curseur temporel
timeslider.toolbar.returnbutton = Retour à ce Pad.
timeslider.toolbar.authors = Auteurs:
timeslider.toolbar.authorsList = Aucun auteurs
timeslider.exportCurrent = Exporter version actuelle vers:

79
src/locales/nl.ini Normal file
View file

@ -0,0 +1,79 @@
; Exported from translatewiki.net
; Author: Siebrand
[nl]
index.newPad = Nieuw pad
index.createOpenPad = Maak of open pad met de naam:
pad.toolbar.bold.title = Vet (Ctrl-B)
pad.toolbar.italic.title = Cursief (Ctrl-I)
pad.toolbar.underline.title = Onderstrepen (Ctrl-U)
pad.toolbar.strikethrough.title = Doorhalen
pad.toolbar.ol.title = Geordende lijst
pad.toolbar.ul.title = Ongeordende lijst
pad.toolbar.indent.title = Inspringen
pad.toolbar.unindent.title = Inspringing verkleinen
pad.toolbar.undo.title = Ongedaan maken (Ctrl-Z)
pad.toolbar.redo.title = Opnieuw uitvoeren (Ctrl-Y)
pad.toolbar.clearAuthorship.title = Kleuren auteurs wissen
pad.toolbar.import_export.title = Naar/van andere opmaak exporteren/importeren
pad.toolbar.timeslider.title = Tijdlijn
pad.toolbar.savedRevision.title = Opgeslagen versies
pad.toolbar.settings.title = Instellingen
pad.toolbar.embed.title = Pad insluiten
pad.toolbar.showusers.title = Gebruikers van dit pad weergeven
pad.colorpicker.save = Opslaan
pad.colorpicker.cancel = Annuleren
pad.loading = Bezig met laden…
pad.settings.padSettings = Padinstellingen
pad.settings.myView = Mijn overzicht
pad.settings.stickychat = Chat altijd zichtbaar
pad.settings.colorcheck = Kleuren auteurs
pad.settings.linenocheck = Regelnummers
pad.settings.fontType = Lettertype:
pad.settings.fontType.normal = Normaal
pad.settings.fontType.monospaced = Monospace
pad.settings.globalView = Globaal overzicht
pad.settings.language = Taal:
pad.importExport.import_export = Importeren/exporteren
pad.importExport.import = Upload een tekstbestand of document
pad.importExport.successful = Afgerond
; Fuzzy
pad.importExport.export = Huidige pad exporteren als
pad.importExport.exporthtml = HTML
pad.importExport.exportplain = Tekst zonder opmaak
pad.importExport.exportword = Microsoft Word
pad.importExport.exportpdf = PDF
pad.importExport.exportopen = ODF (Open Document Format)
pad.importExport.exportdokuwiki = DokuWiki
pad.modals.connected = Verbonden.
pad.modals.reconnecting = Opnieuw verbinding maken met uw pad...
pad.modals.forcereconnect = Opnieuw verbinden
pad.modals.uderdup = Openen in ander venster
pad.modals.userdup.explanation = Dit pad is meer dan één keer geopend in een browservenster op deze computer.
pad.modals.userdup.advice = Opnieuw verbinden en dit venster gebruiken.
pad.modals.unauth = Niet toegestaan
pad.modals.unauth.explanation = Uw rechten zijn gewijzigd terwijl u de pagina aan het bekijken was. Probeer opnieuw te verbinden.
pad.modals.looping = Verbinding verbroken.
pad.modals.looping.explanation = Er is een probleem opgetreden tijdens de communicatie met de synchronisatieserver.
pad.modals.looping.cause = Mogelijk gebruikt de server een niet compatibele firewall of proxy server.
pad.modals.initsocketfail = Server is niet bereikbaar.
pad.modals.initsocketfail.explanation = Het was niet mogelijk te verbinden met de synchronisatieserver.
pad.modals.initsocketfail.cause = Mogelijk komt dit door uw browser of internetverbinding.
pad.modals.slowcommit = Verbinding verbroken.
pad.modals.slowcommit.explanation = De server reageert niet.
pad.modals.slowcommit.cause = Dit komt mogelijk door netwerkproblemen.
pad.modals.deleted = Verwijderd.
pad.modals.deleted.explanation = Dit pad is verwijderd.
pad.modals.disconnected = Uw verbinding is verbroken.
pad.modals.disconnected.explanation = De verbinding met de server is verbroken
pad.modals.disconnected.cause = De server is mogelijk niet beschikbaar. Stel alstublieft de beheerder op de hoogte.
pad.share = Pad delen
pad.share.readonly = Alleen-lezen
pad.share.link = Verwijzing
pad.share.emebdcode = URL insluiten
pad.chat = Chatten
pad.chat.title = Chat voor dit pad opnenen
timeslider.pageTitle = Tijdlijn voor {{appTitle}}
timeslider.toolbar.returnbutton = Terug naar pad
timeslider.toolbar.authors = Auteurs:
timeslider.toolbar.authorsList = Geen auteurs
timeslider.exportCurrent = Huidige versie exporteren als:

View file

@ -619,7 +619,7 @@ exports.updatePadClients = function(pad, callback)
//https://github.com/caolan/async#whilst
//send them all new changesets
async.whilst(
function (){ return sessioninfos[session].rev < pad.getHeadRevisionNumber()},
function (){ return sessioninfos[session] && sessioninfos[session].rev < pad.getHeadRevisionNumber()},
function(callback)
{
var author, revChangeset, currentTime;

View file

@ -0,0 +1,47 @@
var path = require("path")
, npm = require("npm")
, fs = require("fs");
exports.expressCreateServer = function (hook_name, args, cb) {
args.app.get('/tests/frontend/specs_list.js', function(req, res){
fs.readdir('tests/frontend/specs', function(err, files){
if(err){ return res.send(500); }
res.send("var specs_list = " + JSON.stringify(files.sort()) + ";\n");
});
});
var url2FilePath = function(url){
var subPath = url.substr("/tests/frontend".length);
if (subPath == ""){
subPath = "index.html"
}
subPath = subPath.split("?")[0];
var filePath = path.normalize(npm.root + "/../tests/frontend/")
filePath += subPath.replace("..", "");
return filePath;
}
args.app.get('/tests/frontend/specs/*', function (req, res) {
var specFilePath = url2FilePath(req.url);
var specFileName = path.basename(specFilePath);
fs.readFile(specFilePath, function(err, content){
if(err){ return res.send(500); }
content = "describe(" + JSON.stringify(specFileName) + ", function(){ " + content + " });";
res.send(content);
});
});
args.app.get('/tests/frontend/*', function (req, res) {
var filePath = url2FilePath(req.url);
res.sendfile(filePath);
});
args.app.get('/tests/frontend', function (req, res) {
res.redirect('/tests/frontend/');
});
}

35
src/node/hooks/i18n.js Normal file
View file

@ -0,0 +1,35 @@
var languages = require('languages')
, fs = require('fs')
, path = require('path')
, express = require('express')
var localesPath = __dirname+"/../../locales";
// Serve English strings directly with /locales.ini
var localeIndex = fs.readFileSync(localesPath+'/en.ini')+'\r\n';
// add language base 'en' to availableLangs
exports.availableLangs = {en: languages.getLanguageInfo('en')}
fs.readdir(localesPath, function(er, files) {
files.forEach(function(locale) {
locale = locale.split('.')[0]
if(locale.toLowerCase() == 'en') return;
// build locale index
localeIndex += '['+locale+']\r\n@import url(locales/'+locale+'.ini)\r\n'
// add info language {name, nativeName, direction} to availableLangs
exports.availableLangs[locale]=languages.getLanguageInfo(locale);
})
})
exports.expressCreateServer = function(n, args) {
args.app.use('/locales', express.static(localesPath));
args.app.get('/locales.ini', function(req, res) {
res.send(localeIndex);
})
}

View file

@ -7,7 +7,7 @@ module.exports = function (req, res, callback) {
// FIXME: Why is this ever undefined??
if (req.cookies === undefined) req.cookies = {};
securityManager.checkAccess(req.params.pad, req.cookies.sessionid, req.cookies.token, req.cookies.password, function(err, accessObj) {
securityManager.checkAccess(req.params.pad, req.cookies.sessionID, req.cookies.token, req.cookies.password, function(err, accessObj) {
if(ERR(err, callback)) return;
//there is access, continue

View file

@ -17,7 +17,7 @@
"resolve" : "0.2.x",
"socket.io" : "0.9.x",
"ueberDB" : "0.1.8",
"async" : "0.1.22",
"async" : "0.1.x",
"express" : "3.x",
"connect" : "2.4.x",
"clean-css" : "0.3.2",
@ -35,14 +35,16 @@
"security" : "1.0.0",
"tinycon" : "0.0.1",
"underscore" : "1.3.1",
"unorm" : "1.0.0"
"unorm" : "1.0.0",
"languages" : "0.1.1"
},
"bin": { "etherpad-lite": "./node/server.js" },
"devDependencies": {
"jshint" : "*"
"jshint" : "*",
"wd" : "0.0.26"
},
"engines" : { "node" : ">=0.6.0",
"npm" : ">=1.0"
},
"version" : "1.1.4"
"version" : "1.2.0"
}

View file

@ -126,7 +126,7 @@ body.doesWrap {
.sidedivdelayed { /* class set after sizes are set */
background-color: #eee;
color: #888 !important;
border-right: 1px solid #999;
border-right: 1px solid #ccc;
}
.sidedivhidden {
display: none;

View file

@ -748,6 +748,15 @@ input[type=checkbox] {
.popup p {
margin: 5px 0
}
.popup select {
background: #fff;
padding: 2px;
height: 24px;
border-radius: 3px;
border: 1px solid #ccc;
outline: none;
min-width: 105px;
}
.column {
float: left;
width: 50%;

View file

@ -150,7 +150,7 @@ var chat = (function()
$("#chatinput").keypress(function(evt)
{
//if the user typed enter, fire the send
if(evt.which == 13)
if(evt.which == 13 || evt.which == 10)
{
evt.preventDefault();
self.send();

1028
src/static/js/l10n.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -111,6 +111,7 @@ function getParams()
var IsnoColors = params["noColors"];
var rtl = params["rtl"];
var alwaysShowChat = params["alwaysShowChat"];
var lang = params["lang"];
if(IsnoColors)
{
@ -173,6 +174,13 @@ function getParams()
chat.stickToScreen();
}
}
if(lang)
{
if(lang !== "")
{
document.webL10n.setLanguage(lang);
}
}
}
function getUrlVars()
@ -389,6 +397,10 @@ function handshake()
});
// Bind the colorpicker
var fb = $('#colorpicker').farbtastic({ callback: '#mycolorpickerpreview', width: 220});
// Bind the read only button
$('#readonlyinput').on('click',function(){
padeditbar.setEmbedLinks();
});
}
var pad = {
@ -447,6 +459,7 @@ var pad = {
{
pad.collabClient.sendClientMessage(msg);
},
createCookie: createCookie,
init: function()
{

View file

@ -75,6 +75,11 @@ var padeditor = (function()
{
pad.changeViewOption('useMonospaceFont', $("#viewfontmenu").val() == 'monospace');
});
$("#languagemenu").val(document.webL10n.getLanguage());
$("#languagemenu").change(function() {
pad.createCookie("language",$("#languagemenu").val(),null,'/');
document.webL10n.setLanguage($("#languagemenu").val());
});
},
setViewOptions: function(newOptions)
{

View file

@ -23,8 +23,8 @@
<h1>Etherpad Lite Settings</h1>
<a href='https://github.com/Pita/etherpad-lite/wiki/Example-Production-Settings.JSON'>Example production settings template</a>
<a href='https://github.com/Pita/etherpad-lite/wiki/Example-Development-Settings.JSON'>Example development settings template</a>
<a href='https://github.com/ether/etherpad-lite/wiki/Example-Production-Settings.JSON'>Example production settings template</a>
<a href='https://github.com/ether/etherpad-lite/wiki/Example-Development-Settings.JSON'>Example development settings template</a>
<textarea class="settings"></textarea>
<input type="button" class="settingsButton" id="saveSettings" value="Save Settings">
<input type="button" class="settingsButton" id="restartEtherpad" value="Restart Etherpad">

View file

@ -31,8 +31,17 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">
<link rel="resource" type="application/l10n" href="locales.ini" />
<link rel="shortcut icon" href="<%=settings.favicon%>">
<script type="text/javascript">
(function(document) {
// Set language for l10n
var language = document.cookie.match(/language=(\w{2})/);
if(language) document.documentElement.lang = language[1];
})(document)
</script>
<script type="text/javascript" src="static/js/l10n.js" async></script>
<style>
html, body {
@ -148,8 +157,8 @@
<div id="wrapper">
<div id="inner">
<div id="button" onclick="go2Random()" class="translate">New Pad</div>
<div id="label" class="translate">or create/open a Pad with the name</div>
<div id="button" onclick="go2Random()" data-l10n-id="index.newPad"></div>
<div id="label" data-l10n-id="index.createOpenPad"></div>
<form action="#" onsubmit="go2Name();return false;">
<input type="text" id="padname" autofocus x-webkit-speech>
<button type="submit">OK</button>
@ -187,5 +196,4 @@
// start the custom js
if (typeof customStart == "function") customStart();
</script>
</html>

View file

@ -1,5 +1,6 @@
<%
var settings = require("ep_etherpad-lite/node/utils/Settings");
var settings = require("ep_etherpad-lite/node/utils/Settings")
, langs = require("ep_etherpad-lite/node/hooks/i18n").availableLangs
%>
<!doctype html>
<html>
@ -31,9 +32,18 @@
<meta charset="utf-8">
<meta name="robots" content="noindex, nofollow">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">
<link rel="shortcut icon" href="<%=settings.favicon%>">
<link rel="resource" type="application/l10n" href="../locales.ini" />
<script type="text/javascript">
(function() {
// Set language for l10n
var language = document.cookie.match(/language=(\w{2})/);
if(language) document.documentElement.lang = language[1];
})();
</script>
<script type="text/javascript" src="../static/js/l10n.js" async></script>
<% e.begin_block("styles"); %>
<link href="../static/css/pad.css" rel="stylesheet">
<link href="../static/custom/pad.css" rel="stylesheet">
@ -50,60 +60,60 @@
<ul class="menu_left">
<% e.begin_block("editbarMenuLeft"); %>
<li class="acl-write" id="bold" data-key="bold">
<a class="grouped-left" title="Bold (ctrl-B)">
<a class="grouped-left" data-l10n-id="pad.toolbar.bold">
<span class="buttonicon buttonicon-bold"></span>
</a>
</li>
<li class="acl-write" id="italic" data-key="italic">
<a class="grouped-middle" title="Italics (ctrl-I)">
<a class="grouped-middle" data-l10n-id="pad.toolbar.italic">
<span class="buttonicon buttonicon-italic"></span>
</a>
</li>
<li class="acl-write" id="underline" data-key="underline">
<a class="grouped-middle" title="Underline (ctrl-U)">
<a class="grouped-middle" data-l10n-id="pad.toolbar.underline">
<span class="buttonicon buttonicon-underline"></span>
</a>
</li>
<li class="acl-write" id="strikethrough" data-key="strikethrough">
<a class="grouped-right" title="Strikethrough">
<a class="grouped-right" data-l10n-id="pad.toolbar.strikethrough">
<span class="buttonicon buttonicon-strikethrough"></span>
</a>
</li>
<li class="acl-write separator"></li>
<li class="acl-write" id="oderedlist" data-key="insertorderedlist">
<a class="grouped-left" title="Toggle Ordered List">
<a class="grouped-left" data-l10n-id="pad.toolbar.ol">
<span class="buttonicon buttonicon-insertorderedlist"></span>
</a>
</li>
<li class="acl-write" id="unoderedlist" data-key="insertunorderedlist">
<a class="grouped-middle" title="Toggle Bullet List">
<a class="grouped-middle" data-l10n-id="pad.toolbar.ul">
<span class="buttonicon buttonicon-insertunorderedlist"></span>
</a>
</li>
<li class="acl-write" id="indent" data-key="indent">
<a class="grouped-middle" title="Indent">
<a class="grouped-middle" data-l10n-id="pad.toolbar.indent">
<span class="buttonicon buttonicon-indent"></span>
</a>
</li>
<li class="acl-write" id="outdent" data-key="outdent">
<a class="grouped-right" title="Unindent">
<a class="grouped-right" data-l10n-id="pad.toolbar.unindent">
<span class="buttonicon buttonicon-outdent"></span>
</a>
</li>
<li class="acl-write separator"></li>
<li class="acl-write" id="undo" data-key="undo">
<a class="grouped-left" title="Undo (ctrl-Z)">
<a class="grouped-left" data-l10n-id="pad.toolbar.undo">
<span class="buttonicon buttonicon-undo"></span>
</a>
</li>
<li class="acl-write" id="redo" data-key="redo">
<a class="grouped-right" title="Redo (ctrl-Y)">
<a class="grouped-right" data-l10n-id="pad.toolbar.redo">
<span class="buttonicon buttonicon-redo"></span>
</a>
</li>
<li class="acl-write separator"></li>
<li class="acl-write" id="clearAuthorship" data-key="clearauthorship">
<a title="Clear Authorship Colors">
<a data-l10n-id="pad.toolbar.clearAuthorship">
<span class="buttonicon buttonicon-clearauthorship"></span>
</a>
</li>
@ -112,34 +122,34 @@
<ul class="menu_right">
<% e.begin_block("editbarMenuRight"); %>
<li data-key="import_export">
<a class="grouped-left" id="importexportlink" title="Import/Export from/to different document formats">
<a class="grouped-left" id="importexportlink" data-l10n-id="pad.toolbar.import_export">
<span class="buttonicon buttonicon-import_export"></span>
</a>
</li>
<li onClick="document.location = document.location.pathname+ '/timeslider'">
<a id="timesliderlink" class="grouped-middle" title="Show the history of this pad">
<a id="timesliderlink" class="grouped-middle" data-l10n-id="pad.toolbar.timeslider">
<span class="buttonicon buttonicon-history"></span>
</a>
</li>
<li class="acl-write" data-key="savedRevision">
<a class="grouped-right" id="revisionlink" title="Mark this revision as a saved revision">
<a class="grouped-right" id="revisionlink" data-l10n-id="pad.toolbar.savedRevision">
<span class="buttonicon buttonicon-savedRevision"></span>
</a>
</li>
<li class="acl-write separator"></li>
<li class="acl-write" data-key="settings">
<a class="grouped-left" id="settingslink" title="Settings of this pad">
<a class="grouped-left" id="settingslink" data-l10n-id="pad.toolbar.settings">
<span class="buttonicon buttonicon-settings"></span>
</a>
</li>
<li data-key="embed">
<a class="grouped-right" id="embedlink" title="Share and Embed this pad">
<a class="grouped-right" id="embedlink" data-l10n-id="pad.toolbar.embed">
<span class="grouped-right buttonicon buttonicon-embed"></span>
</a>
</li>
<li class="separator"></li>
<li id="usericon" data-key="showusers">
<a title="Show connected users">
<a data-l10n-id="pad.toolbar.showusers">
<span class="buttonicon buttonicon-showusers"></span>
<span id="online_count">1</span>
</a>
@ -153,8 +163,8 @@
<div id="myuser">
<div id="mycolorpicker">
<div id="colorpicker"></div>
<button id="mycolorpickersave">Save</button>
<button id="mycolorpickercancel">Cancel</button>
<button id="mycolorpickersave" data-l10n-id="pad.colorpicker.save"></button>
<button id="mycolorpickercancel" data-l10n-id="pad.colorpicker.cancel"></button>
<span id="mycolorpickerpreview" class="myswatchboxhoverable"></span>
</div>
<div id="myswatchbox"><div id="myswatch"></div></div>
@ -174,56 +184,76 @@
<div id="editorcontainerbox">
<div id="editorcontainer"></div>
<div id="editorloadingbox">
<p>Loading...</p>
<p data-l10n-id="pad.loading">Loading...</p>
<noscript><strong>Sorry, you have to enable Javascript in order to use this.</strong></noscript>
</div>
</div>
<div id="settings" class="popup">
<h1>Pad settings</h1>
<h1 data-l10n-id="pad.settings.padSettings"></h1>
<div class="column">
<% e.begin_block("mySettings"); %>
<h2>My view</h2>
<h2 data-l10n-id="pad.settings.myView"></h2>
<p>
<input type="checkbox" id="options-stickychat" onClick="chat.stickToScreen();">
<label for="options-stickychat">Chat always on screen</label>
<label for="options-stickychat" data-l10n-id="pad.settings.stickychat"></label>
</p>
<p>
<input type="checkbox" id="options-colorscheck">
<label for="options-colorscheck">Authorship colors</label>
<label for="options-colorscheck" data-l10n-id="pad.settings.colorcheck"></label>
</p>
<p>
<input type="checkbox" id="options-linenoscheck" checked>
<label for="options-linenoscheck">Line numbers</label>
</p>
<p>
Font type:
<select id="viewfontmenu">
<option value="normal">Normal</option>
<option value="monospace">Monospaced</option>
</select>
<label for="options-linenoscheck" data-l10n-id="pad.settings.linenocheck"></label>
</p>
<% e.end_block(); %>
<table>
<% e.begin_block("mySettings.dropdowns"); %>
<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>
</select>
</td>
</tr>
<tr>
<td>
<label for="languagemenu" data-l10n-id="pad.settings.language">Language:</label>
</td>
<td>
<select id="languagemenu">
<% for (lang in langs) { %>
<option value="<%=lang%>"><%=langs[lang].nativeName%></option>
<% } %>
</select>
</td>
</tr>
<% e.end_block(); %>
</table>
</div>
<div class="column">
<% e.begin_block("globalSettings"); %>
<h2>Global view</h2>
<h2 data-l10n-id="pad.settings.globalView"></h2>
<% e.end_block(); %>
</div>
</div>
<div id="importexport" class="popup">
<h1>Import/Export</h1>
<h1 data-l10n-id="pad.importExport.import_export"></h1>
<div class="column acl-write">
<% e.begin_block("importColumn"); %>
<h2>Upload any text file or document</h2><br>
<h2 data-l10n-id="pad.importExport.import"></h2><br>
<form id="importform" method="post" action="" target="importiframe" enctype="multipart/form-data">
<div class="importformdiv" id="importformfilediv">
<input type="file" name="file" size="15" id="importfileinput">
<div class="importmessage" id="importmessagefail"></div>
</div>
<div id="import"></div>
<div class="importmessage" id="importmessagesuccess">Successful!</div>
<div class="importmessage" id="importmessagesuccess" data-l10n-id="pad.importExport.successful"></div>
<div class="importformdiv" id="importformsubmitdiv">
<input type="hidden" name="padId" value="blpmaXT35R">
<span class="nowrap">
@ -236,14 +266,14 @@
<% e.end_block(); %>
</div>
<div class="column">
<h2>Export current pad as</h2>
<h2 data-l10n-id="pad.importExport.export"></h2>
<% e.begin_block("exportColumn"); %>
<a id="exporthtmla" target="_blank" class="exportlink"><div class="exporttype" id="exporthtml">HTML</div></a>
<a id="exportplaina" target="_blank" class="exportlink"><div class="exporttype" id="exportplain">Plain text</div></a>
<a id="exportworda" target="_blank" class="exportlink"><div class="exporttype" id="exportword">Microsoft Word</div></a>
<a id="exportpdfa" target="_blank" class="exportlink"><div class="exporttype" id="exportpdf">PDF</div></a>
<a id="exportopena" target="_blank" class="exportlink"><div class="exporttype" id="exportopen">OpenDocument</div></a>
<a id="exportdokuwikia" target="_blank" class="exportlink"><div class="exporttype" id="exportdokuwiki">DokuWiki text</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>
<a id="exportpdfa" target="_blank" class="exportlink"><div class="exporttype" id="exportpdf" data-l10n-id="pad.importExport.exportpdf"></div></a>
<a id="exportopena" target="_blank" class="exportlink"><div class="exporttype" id="exportopen" data-l10n-id="pad.importExport.exportopen"></div></a>
<a id="exportdokuwikia" target="_blank" class="exportlink"><div class="exporttype" id="exportdokuwiki" data-l10n-id="pad.importExport.exportdokuwiki"></div></a>
<% e.end_block(); %>
</div>
</div>
@ -251,48 +281,48 @@
<div id="connectivity" class="popup">
<% e.begin_block("modals"); %>
<div class="connected visible">
<h2>Connected.</h2>
<h2 data-l10n-id="pad.modals.connected"></h2>
</div>
<div class="reconnecting">
<h1>Reestablishing connection...</h1>
<h1 data-l10n-id="pad.modals.reconnecting"></h1>
<p><img alt="" border="0" src="../static/img/connectingbar.gif" /></p>
</div>
<div class="userdup">
<h1>Opened in another window.</h1>
<h2>You seem to have opened this pad in another browser window.</h2>
<p>If you'd like to use this window instead, you can reconnect.</p>
<button id="forcereconnect">Reconnect Now</button>
<h1 data-l10n-id="pad.modals.uderdup"></h1>
<h2 data-l10n-id="pad.modals.userdup.explanation"></h2>
<p data-l10n-id="pad.modals.connected.advice"></p>
<button id="forcereconnect" data-l10n-id="pad.modals.forcereconnect"></button>
</div>
<div class="unauth">
<h1>No Authorization.</h1>
<p>Your browser's credentials or permissions have changed while viewing this pad. Try reconnecting.</p>
<button id="forcereconnect">Reconnect Now</button>
<h1 data-l10n-id="pad.modals.unauth"></h1>
<p data-l10n-id="pad.modals.unauth.explanation"></p>
<button id="forcereconnect" data-l10n-id="pad.modals.forcereconnect"></button>
</div>
<div class="looping">
<h1>Disconnected.</h1>
<h2>We're having trouble talking to the EtherPad lite synchronization server.</h2>
<p>You may be connecting through an incompatible firewall or proxy server.</p>
<h1 data-l10n-id="pad.modals.looping"></h1>
<h2 data-l10n-id="pad.modals.looping.explanation"></h2>
<p data-l10n-id="pad.modals.looping.cause"></p>
</div>
<div class="initsocketfail">
<h1>Disconnected.</h1>
<h2>We were unable to connect to the EtherPad lite synchronization server.</h2>
<p>This may be due to an incompatibility with your web browser or internet connection.</p>
<h1 data-l10n-id="pad.modals.initsocketfail"></h1>
<h2 data-l10n-id="pad.modals.initsocketfail.explanation"></h2>
<p data-l10n-id="pad.modals.initsocketfail.cause"></p>
</div>
<div class="slowcommit">
<h1>Disconnected.</h1>
<h2>Server not responding.</h2>
<p>This may be due to network connectivity issues or high load on the server.</p>
<button id="forcereconnect">Reconnect Now</button>
<h1 data-l10n-id="pad.modals.slowcommit"></h1>
<h2 data-l10n-id="pad.modals.slowcommit.explanation"></h2>
<p data-l10n-id="pad.modals.slowcommit.cause"></p>
<button id="forcereconnect" data-l10n-id="pad.modals.forcereconnect"></button>
</div>
<div class="deleted">
<h1>Disconnected.</h1>
<p>This pad was deleted.</p>
<h1 data-l10n-id="pad.modals.deleted"></h1>
<p data-l10n-id="pad.modals.deleted.explanation"></p>
</div>
<div class="disconnected">
<h1>Disconnected.</h1>
<h2>Lost connection with the EtherPad lite synchronization server.</h2>
<p>This may be due to a loss of network connectivity. If this continues to happen, please let us know</p>
<button id="forcereconnect">Reconnect Now</button>
<h1 data-l10n-id="pad.modals.disconnected"></h1>
<h2 data-l10n-id="pad.modals.disconnected.explanation"></h2>
<p data-l10n-id="pad.modals.disconnected.cause"></p>
<button id="forcereconnect" data-l10n-id="pad.modals.forcereconnect"></button>
</div>
<form id="reconnectform" method="post" action="/ep/pad/reconnect" accept-charset="UTF-8" style="display: none;">
<input type="hidden" class="padId" name="padId">
@ -305,17 +335,17 @@
<div id="embed" class="popup">
<% e.begin_block("embedPopup"); %>
<div id="embedreadonly" class="right acl-write">
<input type="checkbox" id="readonlyinput" onClick="padeditbar.setEmbedLinks();">
<label for="readonlyinput">Read only</label>
<input type="checkbox" id="readonlyinput">
<label for="readonlyinput" data-l10n-id="pad.share.readonly"></label>
</div>
<h1>Share this pad</h1>
<h1 data-l10n-id="pad.share"></h1>
<div id="linkcode">
<h2>Link</h2>
<h2 data-l10n-id="pad.share.link"></h2>
<input id="linkinput" type="text" value="">
</div>
<br>
<div id="embedcode">
<h2>Embed URL</h2>
<h2 data-l10n-id="pad.share.emebdcode"></h2>
<input id="embedinput" type="text" value="">
</div>
<% e.end_block(); %>
@ -323,14 +353,14 @@
<div id="chatthrob"></div>
<div id="chaticon" title="Open the chat for this pad" onclick="chat.show();return false;">
<span id="chatlabel">Chat</span>
<div id="chaticon" data-l10n-id="pad.chat" onclick="chat.show();return false;">
<span id="chatlabel" data-l10n-id="pad.chat"></span>
<span class="buttonicon buttonicon-chat"></span>
<span id="chatcounter">0</span>
</div>
<div id="chatbox">
<div id="titlebar"><span id ="titlelabel">Chat</span><a id="titlecross" onClick="chat.hide();return false;">-&nbsp;</a></div>
<div id="titlebar"><span id ="titlelabel" data-l10n-id="pad.chat"></span><a id="titlecross" onClick="chat.hide();return false;">-&nbsp;</a></div>
<div id="chattext" class="authorColors"></div>
<div id="chatinputbox">
<form>
@ -345,10 +375,9 @@
<% e.begin_block("scripts"); %>
<script type="text/javascript">
/* Display errors on page load to the user
(Gets overridden by padutils.setupGlobalExceptionHandler)
*/
(function() {
// Display errors on page load to the user
// (Gets overridden by padutils.setupGlobalExceptionHandler)
var originalHandler = window.onerror;
window.onerror = function(msg, url, line) {
var box = document.getElementById('editorloadingbox');
@ -360,7 +389,7 @@
};
})();
</script>
<script type="text/javascript" src="../static/js/require-kernel.js"></script>
<script type="text/javascript" src="../socket.io/socket.io.js"></script>
@ -374,8 +403,6 @@
<script type="text/javascript">
var clientVars = {};
(function () {
var pathComponents = location.pathname.split('/');
// Strip 'p' and the padname from the pathname and set as baseURL

View file

@ -1,9 +1,10 @@
<%
var settings = require("ep_etherpad-lite/node/utils/Settings");
var settings = require("ep_etherpad-lite/node/utils/Settings")
, langs = require("ep_etherpad-lite/node/hooks/i18n").availableLangs
%>
<!doctype html>
<html lang="en">
<title><%=settings.title%> Timeslider</title>
<html>
<title data-l10n-id="timeslider.pageTitle" data-l10n-args='{ "appTitle": "<%=settings.title%>" }'><%=settings.title%> Timeslider</title>
<script>
/*
|@licstart The following is the entire license notice for the
@ -31,6 +32,16 @@
<meta charset="utf-8">
<meta name="robots" content="noindex, nofollow">
<link rel="shortcut icon" href="<%=settings.favicon%>">
<link rel="resource" type="application/l10n" href="../../locales.ini" />
<script type="text/javascript">
(function() {
// Set language for l10n
var language = document.cookie.match(/language=(\w{2})/);
if(language) document.documentElement.lang = language[1];
})();
</script>
<script type="text/javascript" src="../../static/js/l10n.js" async></script>
<link rel="stylesheet" href="../../static/css/pad.css">
<link rel="stylesheet" href="../../static/css/timeslider.css">
<link rel="stylesheet" href="../../static/custom/timeslider.css">
@ -69,12 +80,12 @@
<div class="editbarright toolbar" id="editbar">
<ul>
<li onClick="window.padeditbar.toolbarClick('import_export');return false;">
<a id="exportlink" title="Export to different document formats">
<a id="exportlink" data-l10n-id="pad.importExport.export">
<div class="buttonicon buttonicon-import_export"></div>
</a>
</li>
</ul>
<a id="returnbutton">Return to pad</a>
<a id="returnbutton" data-l10n-id="timeslider.toolbar.returnbutton"></a>
</div>
<div>
@ -82,9 +93,8 @@
<span id="revision_label"></span>
<span id="revision_date"></span>
</h1>
<p>Authors:
<span id="authorsList">
<span>No Authors</span>
<p data-l10n-id="timeslider.toolbar.authors">
<span id="authorsList" data-l10n-id="timeslider.toolbar.authorsList"></span>
</span> </p>
</div>
</div>
@ -101,70 +111,69 @@
</div><!-- /padpage -->
<div id="connectivity" class="popup">
<% e.begin_block("modals"); %>
<% e.begin_block("modals"); %>
<div class="connected visible">
<h2>Connected.</h2>
<h2 data-l10n-id="pad.modals.connected"></h2>
</div>
<div class="reconnecting">
<h1>Reestablishing connection...</h1>
<h1 data-l10n-id="pad.modals.reconnecting"></h1>
<p><img alt="" border="0" src="../../static/img/connectingbar.gif" /></p>
</div>
<div class="userdup">
<h1>Opened in another window.</h1>
<h2>You seem to have opened this pad in another browser window.</h2>
<p>If you'd like to use this window instead, you can reconnect.</p>
<button id="forcereconnect">Reconnect Now</button>
<h1 data-l10n-id="pad.modals.uderdup"></h1>
<h2 data-l10n-id="pad.modals.userdup.explanation"></h2>
<p data-l10n-id="pad.modals.connected.advice"></p>
<button id="forcereconnect" data-l10n-id="pad.modals.forcereconnect"></button>
</div>
<div class="unauth">
<h1>No Authorization.</h1>
<p>Your browser's credentials or permissions have changed while viewing this pad. Try reconnecting.</p>
<button id="forcereconnect">Reconnect Now</button>
<h1 data-l10n-id="pad.modals.unauth"></h1>
<p data-l10n-id="pad.modals.unauth.explanation"></p>
<button id="forcereconnect" data-l10n-id="pad.modals.forcereconnect"></button>
</div>
<div class="looping">
<h1>Disconnected.</h1>
<h2>We're having trouble talking to the EtherPad lite synchronization server.</h2>
<p>You may be connecting through an incompatible firewall or proxy server.</p>
<h1 data-l10n-id="pad.modals.looping"></h1>
<h2 data-l10n-id="pad.modals.looping.explanation"></h2>
<p data-l10n-id="pad.modals.looping.cause"></p>
</div>
<div class="initsocketfail">
<h1>Disconnected.</h1>
<h2>We were unable to connect to the EtherPad lite synchronization server.</h2>
<p>This may be due to an incompatibility with your web browser or internet connection.</p>
<h1 data-l10n-id="pad.modals.initsocketfail"></h1>
<h2 data-l10n-id="pad.modals.initsocketfail.explanation"></h2>
<p data-l10n-id="pad.modals.initsocketfail.cause"></p>
</div>
<div class="slowcommit">
<h1>Disconnected.</h1>
<h2>Server not responding.</h2>
<p>This may be due to network connectivity issues or high load on the server.</p>
<button id="forcereconnect">Reconnect Now</button>
<h1 data-l10n-id="pad.modals.slowcommit"></h1>
<h2 data-l10n-id="pad.modals.slowcommit.explanation"></h2>
<p data-l10n-id="pad.modals.slowcommit.cause"></p>
<button id="forcereconnect" data-l10n-id="pad.modals.forcereconnect"></button>
</div>
<div class="deleted">
<h1>Disconnected.</h1>
<p>This pad was deleted.</p>
<h1 data-l10n-id="pad.modals.deleted"></h1>
<p data-l10n-id="pad.modals.deleted.explanation"></p>
</div>
<div class="disconnected">
<h1>Disconnected.</h1>
<h2>Lost connection with the EtherPad lite synchronization server.</h2>
<p>This may be due to a loss of network connectivity. If this continues to happen, please let us know</p>
<button id="forcereconnect">Reconnect Now</button>
<h1 data-l10n-id="pad.modals.disconnected"></h1>
<h2 data-l10n-id="pad.modals.disconnected.explanation"></h2>
<p data-l10n-id="pad.modals.disconnected.cause"></p>
<button id="forcereconnect" data-l10n-id="pad.modals.forcereconnect"></button>
</div>
<form id="reconnectform" method="post" action="/ep/pad/reconnect" accept-charset="UTF-8" style="display: none;">
<input type="hidden" class="padId" name="padId">
<input type="hidden" class="diagnosticInfo" name="diagnosticInfo">
<input type="hidden" class="missedChanges" name="missedChanges">
</form>
<% e.end_block(); %>
<% e.end_block(); %>
</div>
<!-- export code -->
<div id="importexport">
<div id="export" class="popup">
Export current version as:
<a id="exporthtmla" target="_blank" class="exportlink"><div class="exporttype" id="exporthtml">HTML</div></a>
<a id="exportplaina" target="_blank" class="exportlink"><div class="exporttype" id="exportplain">Plain text</div></a>
<a id="exportworda" target="_blank" class="exportlink"><div class="exporttype" id="exportword">Microsoft Word</div></a>
<a id="exportpdfa" target="_blank" class="exportlink"><div class="exporttype" id="exportpdf">PDF</div></a>
<a id="exportopena" target="_blank" class="exportlink"><div class="exporttype" id="exportopen">OpenDocument</div></a>
<a id="exportdokuwikia" target="_blank" class="exportlink"><div class="exporttype" id="exportdokuwiki">DokuWiki text</div></a>
<div id="export" class="popup" data-l10n-id="timeslider.exportCurrent">
<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>
<a id="exportpdfa" target="_blank" class="exportlink"><div class="exporttype" id="exportpdf" data-l10n-id="pad.importExport.exportpdf"></div></a>
<a id="exportopena" target="_blank" class="exportlink"><div class="exporttype" id="exportopen" data-l10n-id="pad.importExport.exportopen"></div></a>
<a id="exportdokuwikia" target="_blank" class="exportlink"><div class="exporttype" id="exportdokuwiki" data-l10n-id="pad.importExport.exportdokuwiki"></div></a>
</div>
</div>
@ -182,7 +191,6 @@
var clientVars = {};
(function () {
var pathComponents = location.pathname.split('/');
// Strip 'p', the padname and 'timeslider' from the pathname and set as baseURL

159
tests/frontend/helper.js Normal file
View file

@ -0,0 +1,159 @@
var helper = {};
(function(){
var $iframeContainer, $iframe, jsLibraries = {};
helper.init = function(cb){
$iframeContainer = $("#iframe-container");
$.get('/static/js/jquery.js').done(function(code){
// make sure we don't override existing jquery
jsLibraries["jquery"] = "if(typeof $ === 'undefined') {\n" + code + "\n}";
$.get('/tests/frontend/lib/sendkeys.js').done(function(code){
jsLibraries["sendkeys"] = code;
cb();
});
});
}
helper.randomString = function randomString(len)
{
var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
var randomstring = '';
for (var i = 0; i < len; i++)
{
var rnum = Math.floor(Math.random() * chars.length);
randomstring += chars.substring(rnum, rnum + 1);
}
return randomstring;
}
var getFrameJQuery = function($iframe){
/*
I tried over 9000 ways to inject javascript into iframes.
This is the only way I found that worked in IE 7+8+9, FF and Chrome
*/
var win = $iframe[0].contentWindow;
var doc = win.document;
//IE 8+9 Hack to make eval appear
//http://stackoverflow.com/questions/2720444/why-does-this-window-object-not-have-the-eval-function
win.execScript && win.execScript("null");
win.eval(jsLibraries["jquery"]);
win.eval(jsLibraries["sendkeys"]);
win.$.window = win;
win.$.document = doc;
return win.$;
}
helper.clearCookies = function(){
window.document.cookie = "";
}
helper.newPad = function(){
//build opts object
var opts = {clearCookies: true}
if(typeof arguments[0] === 'function'){
opts.cb = arguments[0]
} else {
opts = _.defaults(arguments[0], opts);
}
//clear cookies
if(opts.clearCookies){
helper.clearCookies();
}
var padName = "FRONTEND_TEST_" + helper.randomString(20);
$iframe = $("<iframe src='/p/" + padName + "'></iframe>");
//clean up inner iframe references
helper.padChrome$ = helper.padOuter$ = helper.padInner$ = null;
//clean up iframes properly to prevent IE from memoryleaking
$iframeContainer.find("iframe").purgeFrame().done(function(){
$iframeContainer.append($iframe);
$iframe.one('load', function(){
helper.waitFor(function(){
return !$iframe.contents().find("#editorloadingbox").is(":visible");
}, 50000).done(function(){
helper.padChrome$ = getFrameJQuery( $('#iframe-container iframe'));
helper.padOuter$ = getFrameJQuery(helper.padChrome$('iframe.[name="ace_outer"]'));
helper.padInner$ = getFrameJQuery( helper.padOuter$('iframe.[name="ace_inner"]'));
//disable all animations, this makes tests faster and easier
helper.padChrome$.fx.off = true;
helper.padOuter$.fx.off = true;
helper.padInner$.fx.off = true;
opts.cb();
}).fail(function(){
throw new Error("Pad never loaded");
});
});
});
return padName;
}
helper.waitFor = function(conditionFunc, _timeoutTime, _intervalTime){
var timeoutTime = _timeoutTime || 1000;
var intervalTime = _intervalTime || 10;
var deferred = $.Deferred();
var _fail = deferred.fail;
var listenForFail = false;
deferred.fail = function(){
listenForFail = true;
_fail.apply(this, arguments);
}
var intervalCheck = setInterval(function(){
var passed = false;
passed = conditionFunc();
if(passed){
clearInterval(intervalCheck);
clearTimeout(timeout);
deferred.resolve();
}
}, intervalTime);
var timeout = setTimeout(function(){
clearInterval(intervalCheck);
var error = new Error("wait for condition never became true " + conditionFunc.toString());
deferred.reject(error);
if(!listenForFail){
throw error;
}
}, timeoutTime);
return deferred;
}
/* Ensure console.log doesn't blow up in IE, ugly but ok for a test framework imho*/
window.console = window.console || {};
window.console.log = window.console.log || function(){}
//force usage of callbacks in it
var _it = it;
it = function(name, func){
if(func && func.length !== 1){
func = function(){
throw new Error("Please use always a callback with it() - " + func.toString());
}
}
_it(name, func);
}
})()

26
tests/frontend/index.html Normal file
View file

@ -0,0 +1,26 @@
<!doctype html>
<html>
<title>Frontend tests</title>
<meta charset="utf-8">
<link rel="stylesheet" href="runner.css" />
<div id="console"></div>
<div id="mocha"></div>
<div id="iframe-container"></div>
<script src="/static/js/jquery.js"></script>
<script src="lib/underscore.js"></script>
<script src="lib/mocha.js"></script>
<script> mocha.setup('bdd') </script>
<script src="lib/expect.js"></script>
<script src="lib/sendkeys.js"></script>
<script src="lib/jquery.iframe.js"></script>
<script src="helper.js"></script>
<script src="specs_list.js"></script>
<script src="runner.js"></script>
</html>

1247
tests/frontend/lib/expect.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,39 @@
//copied from http://stackoverflow.com/questions/8407946/is-it-possible-to-use-iframes-in-ie-without-memory-leaks
(function($) {
$.fn.purgeFrame = function() {
var deferred;
if ($.browser.msie && parseFloat($.browser.version, 10) < 9) {
deferred = purge(this);
} else {
this.remove();
deferred = $.Deferred();
deferred.resolve();
}
return deferred;
};
function purge($frame) {
var sem = $frame.length
, deferred = $.Deferred();
$frame.load(function() {
var frame = this;
frame.contentWindow.document.innerHTML = '';
sem -= 1;
if (sem <= 0) {
$frame.remove();
deferred.resolve();
}
});
$frame.attr('src', 'about:blank');
if ($frame.length === 0) {
deferred.resolve();
}
return deferred.promise();
}
})(jQuery);

4868
tests/frontend/lib/mocha.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,467 @@
// Cross-broswer implementation of text ranges and selections
// documentation: http://bililite.com/blog/2011/01/11/cross-browser-.and-selections/
// Version: 1.1
// Copyright (c) 2010 Daniel Wachsstock
// MIT license:
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
(function($){
bililiteRange = function(el, debug){
var ret;
if (debug){
ret = new NothingRange(); // Easier to force it to use the no-selection type than to try to find an old browser
}else if (document.selection && !document.addEventListener){
// Internet Explorer 8 and lower
ret = new IERange();
}else if (window.getSelection && el.setSelectionRange){
// Standards. Element is an input or textarea
ret = new InputRange();
}else if (window.getSelection){
// Standards, with any other kind of element
ret = new W3CRange()
}else{
// doesn't support selection
ret = new NothingRange();
}
ret._el = el;
ret._doc = el.ownerDocument;
ret._win = 'defaultView' in ret._doc ? ret._doc.defaultView : ret._doc.parentWindow;
ret._textProp = textProp(el);
ret._bounds = [0, ret.length()];
return ret;
}
function textProp(el){
// returns the property that contains the text of the element
if (typeof el.value != 'undefined') return 'value';
if (typeof el.text != 'undefined') return 'text';
if (typeof el.textContent != 'undefined') return 'textContent';
return 'innerText';
}
// base class
function Range(){}
Range.prototype = {
length: function() {
return this._el[this._textProp].replace(/\r/g, '').length; // need to correct for IE's CrLf weirdness
},
bounds: function(s){
if (s === 'all'){
this._bounds = [0, this.length()];
}else if (s === 'start'){
this._bounds = [0, 0];
}else if (s === 'end'){
this._bounds = [this.length(), this.length()];
}else if (s === 'selection'){
this.bounds ('all'); // first select the whole thing for constraining
this._bounds = this._nativeSelection();
}else if (s){
this._bounds = s; // don't error check now; the element may change at any moment, so constrain it when we need it.
}else{
var b = [
Math.max(0, Math.min (this.length(), this._bounds[0])),
Math.max(0, Math.min (this.length(), this._bounds[1]))
];
return b; // need to constrain it to fit
}
return this; // allow for chaining
},
select: function(){
this._nativeSelect(this._nativeRange(this.bounds()));
return this; // allow for chaining
},
text: function(text, select){
if (arguments.length){
this._nativeSetText(text, this._nativeRange(this.bounds()));
if (select == 'start'){
this.bounds ([this._bounds[0], this._bounds[0]]);
this.select();
}else if (select == 'end'){
this.bounds ([this._bounds[0]+text.length, this._bounds[0]+text.length]);
this.select();
}else if (select == 'all'){
this.bounds ([this._bounds[0], this._bounds[0]+text.length]);
this.select();
}
return this; // allow for chaining
}else{
return this._nativeGetText(this._nativeRange(this.bounds()));
}
},
insertEOL: function (){
this._nativeEOL();
this._bounds = [this._bounds[0]+1, this._bounds[0]+1]; // move past the EOL marker
return this;
}
};
function IERange(){}
IERange.prototype = new Range();
IERange.prototype._nativeRange = function (bounds){
var rng;
if (this._el.tagName == 'INPUT'){
// IE 8 is very inconsistent; textareas have createTextRange but it doesn't work
rng = this._el.createTextRange();
}else{
rng = this._doc.body.createTextRange ();
rng.moveToElementText(this._el);
}
if (bounds){
if (bounds[1] < 0) bounds[1] = 0; // IE tends to run elements out of bounds
if (bounds[0] > this.length()) bounds[0] = this.length();
if (bounds[1] < rng.text.replace(/\r/g, '').length){ // correct for IE's CrLf wierdness
// block-display elements have an invisible, uncounted end of element marker, so we move an extra one and use the current length of the range
rng.moveEnd ('character', -1);
rng.moveEnd ('character', bounds[1]-rng.text.replace(/\r/g, '').length);
}
if (bounds[0] > 0) rng.moveStart('character', bounds[0]);
}
return rng;
};
IERange.prototype._nativeSelect = function (rng){
rng.select();
};
IERange.prototype._nativeSelection = function (){
// returns [start, end] for the selection constrained to be in element
var rng = this._nativeRange(); // range of the element to constrain to
var len = this.length();
if (this._doc.selection.type != 'Text') return [0,0]; // append to the end
var sel = this._doc.selection.createRange();
try{
return [
iestart(sel, rng),
ieend (sel, rng)
];
}catch (e){
// IE gets upset sometimes about comparing text to input elements, but the selections cannot overlap, so make a best guess
return (sel.parentElement().sourceIndex < this._el.sourceIndex) ? [0,0] : [len, len];
}
};
IERange.prototype._nativeGetText = function (rng){
return rng.text.replace(/\r/g, ''); // correct for IE's CrLf weirdness
};
IERange.prototype._nativeSetText = function (text, rng){
rng.text = text;
};
IERange.prototype._nativeEOL = function(){
if (typeof this._el.value != 'undefined'){
this.text('\n'); // for input and textarea, insert it straight
}else{
this._nativeRange(this.bounds()).pasteHTML('<br/>');
}
};
// IE internals
function iestart(rng, constraint){
// returns the position (in character) of the start of rng within constraint. If it's not in constraint, returns 0 if it's before, length if it's after
var len = constraint.text.replace(/\r/g, '').length; // correct for IE's CrLf wierdness
if (rng.compareEndPoints ('StartToStart', constraint) <= 0) return 0; // at or before the beginning
if (rng.compareEndPoints ('StartToEnd', constraint) >= 0) return len;
for (var i = 0; rng.compareEndPoints ('StartToStart', constraint) > 0; ++i, rng.moveStart('character', -1));
return i;
}
function ieend (rng, constraint){
// returns the position (in character) of the end of rng within constraint. If it's not in constraint, returns 0 if it's before, length if it's after
var len = constraint.text.replace(/\r/g, '').length; // correct for IE's CrLf wierdness
if (rng.compareEndPoints ('EndToEnd', constraint) >= 0) return len; // at or after the end
if (rng.compareEndPoints ('EndToStart', constraint) <= 0) return 0;
for (var i = 0; rng.compareEndPoints ('EndToStart', constraint) > 0; ++i, rng.moveEnd('character', -1));
return i;
}
// an input element in a standards document. "Native Range" is just the bounds array
function InputRange(){}
InputRange.prototype = new Range();
InputRange.prototype._nativeRange = function(bounds) {
return bounds || [0, this.length()];
};
InputRange.prototype._nativeSelect = function (rng){
this._el.setSelectionRange(rng[0], rng[1]);
};
InputRange.prototype._nativeSelection = function(){
return [this._el.selectionStart, this._el.selectionEnd];
};
InputRange.prototype._nativeGetText = function(rng){
return this._el.value.substring(rng[0], rng[1]);
};
InputRange.prototype._nativeSetText = function(text, rng){
var val = this._el.value;
this._el.value = val.substring(0, rng[0]) + text + val.substring(rng[1]);
};
InputRange.prototype._nativeEOL = function(){
this.text('\n');
};
function W3CRange(){}
W3CRange.prototype = new Range();
W3CRange.prototype._nativeRange = function (bounds){
var rng = this._doc.createRange();
rng.selectNodeContents(this._el);
if (bounds){
w3cmoveBoundary (rng, bounds[0], true, this._el);
rng.collapse (true);
w3cmoveBoundary (rng, bounds[1]-bounds[0], false, this._el);
}
return rng;
};
W3CRange.prototype._nativeSelect = function (rng){
this._win.getSelection().removeAllRanges();
this._win.getSelection().addRange (rng);
};
W3CRange.prototype._nativeSelection = function (){
// returns [start, end] for the selection constrained to be in element
var rng = this._nativeRange(); // range of the element to constrain to
if (this._win.getSelection().rangeCount == 0) return [this.length(), this.length()]; // append to the end
var sel = this._win.getSelection().getRangeAt(0);
return [
w3cstart(sel, rng),
w3cend (sel, rng)
];
}
W3CRange.prototype._nativeGetText = function (rng){
return rng.toString();
};
W3CRange.prototype._nativeSetText = function (text, rng){
rng.deleteContents();
rng.insertNode (this._doc.createTextNode(text));
this._el.normalize(); // merge the text with the surrounding text
};
W3CRange.prototype._nativeEOL = function(){
var rng = this._nativeRange(this.bounds());
rng.deleteContents();
var br = this._doc.createElement('br');
br.setAttribute ('_moz_dirty', ''); // for Firefox
rng.insertNode (br);
rng.insertNode (this._doc.createTextNode('\n'));
rng.collapse (false);
};
// W3C internals
function nextnode (node, root){
// in-order traversal
// we've already visited node, so get kids then siblings
if (node.firstChild) return node.firstChild;
if (node.nextSibling) return node.nextSibling;
if (node===root) return null;
while (node.parentNode){
// get uncles
node = node.parentNode;
if (node == root) return null;
if (node.nextSibling) return node.nextSibling;
}
return null;
}
function w3cmoveBoundary (rng, n, bStart, el){
// move the boundary (bStart == true ? start : end) n characters forward, up to the end of element el. Forward only!
// if the start is moved after the end, then an exception is raised
if (n <= 0) return;
var node = rng[bStart ? 'startContainer' : 'endContainer'];
if (node.nodeType == 3){
// we may be starting somewhere into the text
n += rng[bStart ? 'startOffset' : 'endOffset'];
}
while (node){
if (node.nodeType == 3){
if (n <= node.nodeValue.length){
rng[bStart ? 'setStart' : 'setEnd'](node, n);
// special case: if we end next to a <br>, include that node.
if (n == node.nodeValue.length){
// skip past zero-length text nodes
for (var next = nextnode (node, el); next && next.nodeType==3 && next.nodeValue.length == 0; next = nextnode(next, el)){
rng[bStart ? 'setStartAfter' : 'setEndAfter'](next);
}
if (next && next.nodeType == 1 && next.nodeName == "BR") rng[bStart ? 'setStartAfter' : 'setEndAfter'](next);
}
return;
}else{
rng[bStart ? 'setStartAfter' : 'setEndAfter'](node); // skip past this one
n -= node.nodeValue.length; // and eat these characters
}
}
node = nextnode (node, el);
}
}
var START_TO_START = 0; // from the w3c definitions
var START_TO_END = 1;
var END_TO_END = 2;
var END_TO_START = 3;
// from the Mozilla documentation, for range.compareBoundaryPoints(how, sourceRange)
// -1, 0, or 1, indicating whether the corresponding boundary-point of range is respectively before, equal to, or after the corresponding boundary-point of sourceRange.
// * Range.END_TO_END compares the end boundary-point of sourceRange to the end boundary-point of range.
// * Range.END_TO_START compares the end boundary-point of sourceRange to the start boundary-point of range.
// * Range.START_TO_END compares the start boundary-point of sourceRange to the end boundary-point of range.
// * Range.START_TO_START compares the start boundary-point of sourceRange to the start boundary-point of range.
function w3cstart(rng, constraint){
if (rng.compareBoundaryPoints (START_TO_START, constraint) <= 0) return 0; // at or before the beginning
if (rng.compareBoundaryPoints (END_TO_START, constraint) >= 0) return constraint.toString().length;
rng = rng.cloneRange(); // don't change the original
rng.setEnd (constraint.endContainer, constraint.endOffset); // they now end at the same place
return constraint.toString().length - rng.toString().length;
}
function w3cend (rng, constraint){
if (rng.compareBoundaryPoints (END_TO_END, constraint) >= 0) return constraint.toString().length; // at or after the end
if (rng.compareBoundaryPoints (START_TO_END, constraint) <= 0) return 0;
rng = rng.cloneRange(); // don't change the original
rng.setStart (constraint.startContainer, constraint.startOffset); // they now start at the same place
return rng.toString().length;
}
function NothingRange(){}
NothingRange.prototype = new Range();
NothingRange.prototype._nativeRange = function(bounds) {
return bounds || [0,this.length()];
};
NothingRange.prototype._nativeSelect = function (rng){ // do nothing
};
NothingRange.prototype._nativeSelection = function(){
return [0,0];
};
NothingRange.prototype._nativeGetText = function (rng){
return this._el[this._textProp].substring(rng[0], rng[1]);
};
NothingRange.prototype._nativeSetText = function (text, rng){
var val = this._el[this._textProp];
this._el[this._textProp] = val.substring(0, rng[0]) + text + val.substring(rng[1]);
};
NothingRange.prototype._nativeEOL = function(){
this.text('\n');
};
})(jQuery);
// insert characters in a textarea or text input field
// special characters are enclosed in {}; use {{} for the { character itself
// documentation: http://bililite.com/blog/2008/08/20/the-fnsendkeys-plugin/
// Version: 2.0
// Copyright (c) 2010 Daniel Wachsstock
// MIT license:
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
(function($){
$.fn.sendkeys = function (x, opts){
return this.each( function(){
var localkeys = $.extend({}, opts, $(this).data('sendkeys')); // allow for element-specific key functions
// most elements to not keep track of their selection when they lose focus, so we have to do it for them
var rng = $.data (this, 'sendkeys.selection');
if (!rng){
rng = bililiteRange(this).bounds('selection');
$.data(this, 'sendkeys.selection', rng);
$(this).bind('mouseup.sendkeys', function(){
// we have to update the saved range. The routines here update the bounds with each press, but actual keypresses and mouseclicks do not
$.data(this, 'sendkeys.selection').bounds('selection');
}).bind('keyup.sendkeys', function(evt){
// restore the selection if we got here with a tab (a click should select what was clicked on)
if (evt.which == 9){
// there's a flash of selection when we restore the focus, but I don't know how to avoid that.
$.data(this, 'sendkeys.selection').select();
}else{
$.data(this, 'sendkeys.selection').bounds('selection');
}
});
}
this.focus();
if (typeof x === 'undefined') return; // no string, so we just set up the event handlers
$.data(this, 'sendkeys.originalText', rng.text());
x.replace(/\n/g, '{enter}'). // turn line feeds into explicit break insertions
replace(/{[^}]*}|[^{]+/g, function(s){
(localkeys[s] || $.fn.sendkeys.defaults[s] || $.fn.sendkeys.defaults.simplechar)(rng, s);
});
$(this).trigger({type: 'sendkeys', which: x});
});
}; // sendkeys
// add the functions publicly so they can be overridden
$.fn.sendkeys.defaults = {
simplechar: function (rng, s){
rng.text(s, 'end');
for (var i =0; i < s.length; ++i){
var x = s.charCodeAt(i);
// a bit of cheating: rng._el is the element associated with rng.
$(rng._el).trigger({type: 'keypress', keyCode: x, which: x, charCode: x});
}
},
'{{}': function (rng){
$.fn.sendkeys.defaults.simplechar (rng, '{')
},
'{enter}': function (rng){
rng.insertEOL();
rng.select();
var x = '\n'.charCodeAt(0);
$(rng._el).trigger({type: 'keypress', keyCode: x, which: x, charCode: x});
},
'{backspace}': function (rng){
var b = rng.bounds();
if (b[0] == b[1]) rng.bounds([b[0]-1, b[0]]); // no characters selected; it's just an insertion point. Remove the previous character
rng.text('', 'end'); // delete the characters and update the selection
},
'{del}': function (rng){
var b = rng.bounds();
if (b[0] == b[1]) rng.bounds([b[0], b[0]+1]); // no characters selected; it's just an insertion point. Remove the next character
rng.text('', 'end'); // delete the characters and update the selection
},
'{rightarrow}': function (rng){
var b = rng.bounds();
if (b[0] == b[1]) ++b[1]; // no characters selected; it's just an insertion point. Move to the right
rng.bounds([b[1], b[1]]).select();
},
'{leftarrow}': function (rng){
var b = rng.bounds();
if (b[0] == b[1]) --b[0]; // no characters selected; it's just an insertion point. Move to the left
rng.bounds([b[0], b[0]]).select();
},
'{selectall}' : function (rng){
rng.bounds('all').select();
},
'{selection}': function (rng){
$.fn.sendkeys.defaults.simplechar(rng, $.data(rng._el, 'sendkeys.originalText'));
},
'{mark}' : function (rng){
var bounds = rng.bounds();
$(rng._el).one('sendkeys', function(){
// set up the event listener to change the selection after the sendkeys is done
rng.bounds(bounds).select();
});
}
};
})(jQuery)

File diff suppressed because it is too large Load diff

228
tests/frontend/runner.css Normal file
View file

@ -0,0 +1,228 @@
html {
height: 100%;
}
body {
padding: 0px;
margin: 0px;
height: 100%;
}
#console {
display: none;
}
#iframe-container {
width: 50%;
height: 100%;
float:right;
}
#iframe-container iframe {
width: 100%;
height: 100%;
}
#mocha {
font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif;
border-right: 2px solid #999;
width: 50%;
height: 100%;
position: absolute;
overflow: auto;
float:left;
}
#mocha #report {
margin-top: 50px;
}
#mocha ul, #mocha li {
margin: 0;
padding: 0;
}
#mocha ul {
list-style: none;
}
#mocha h1, #mocha h2 {
margin: 0;
}
#mocha h1 {
margin-top: 15px;
font-size: 1em;
font-weight: 200;
}
#mocha h1 a:visited
{
color: #00E;
}
#mocha .suite .suite h1 {
margin-top: 0;
font-size: .8em;
}
#mocha h2 {
font-size: 12px;
font-weight: normal;
cursor: pointer;
}
#mocha .suite {
margin-left: 15px;
}
#mocha .test {
margin-left: 15px;
}
#mocha .test:hover h2::after {
position: relative;
top: 0;
right: -10px;
content: '(view source)';
font-size: 12px;
font-family: arial;
color: #888;
}
#mocha .test.pending:hover h2::after {
content: '(pending)';
font-family: arial;
}
#mocha .test.pass.medium .duration {
background: #C09853;
}
#mocha .test.pass.slow .duration {
background: #B94A48;
}
#mocha .test.pass::before {
content: '✓';
font-size: 12px;
display: block;
float: left;
margin-right: 5px;
color: #00d6b2;
}
#mocha .test.pass .duration {
font-size: 9px;
margin-left: 5px;
padding: 2px 5px;
color: white;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
-moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
-ms-border-radius: 5px;
-o-border-radius: 5px;
border-radius: 5px;
}
#mocha .test.pass.fast .duration {
display: none;
}
#mocha .test.pending {
color: #0b97c4;
}
#mocha .test.pending::before {
content: '◦';
color: #0b97c4;
}
#mocha .test.fail {
color: #c00;
}
#mocha .test.fail pre {
color: black;
}
#mocha .test.fail::before {
content: '✖';
font-size: 12px;
display: block;
float: left;
margin-right: 5px;
color: #c00;
}
#mocha .test pre.error {
color: #c00;
}
#mocha .test pre {
display: inline-block;
font: 12px/1.5 monaco, monospace;
margin: 5px;
padding: 15px;
border: 1px solid #eee;
border-bottom-color: #ddd;
-webkit-border-radius: 3px;
-webkit-box-shadow: 0 1px 3px #eee;
}
#report.pass .test.fail {
display: none;
}
#report.fail .test.pass {
display: none;
}
#error {
color: #c00;
font-size: 1.5 em;
font-weight: 100;
letter-spacing: 1px;
}
#stats {
position: fixed;
top: 15px;
right: 52%;
font-size: 12px;
margin: 0;
color: #888;
}
#stats .progress {
float: right;
padding-top: 0;
}
#stats em {
color: black;
}
#stats a {
text-decoration: none;
color: inherit;
}
#stats a:hover {
border-bottom: 1px solid #eee;
}
#stats li {
display: inline-block;
margin: 0 5px;
list-style: none;
padding-top: 11px;
}
code .comment { color: #ddd }
code .init { color: #2F6FAD }
code .string { color: #5890AD }
code .keyword { color: #8A6343 }
code .number { color: #2F6FAD }

199
tests/frontend/runner.js Normal file
View file

@ -0,0 +1,199 @@
$(function(){
function Base(runner) {
var self = this
, stats = this.stats = { suites: 0, tests: 0, passes: 0, pending: 0, failures: 0 }
, failures = this.failures = [];
if (!runner) return;
this.runner = runner;
runner.on('start', function(){
stats.start = new Date;
});
runner.on('suite', function(suite){
stats.suites = stats.suites || 0;
suite.root || stats.suites++;
});
runner.on('test end', function(test){
stats.tests = stats.tests || 0;
stats.tests++;
});
runner.on('pass', function(test){
stats.passes = stats.passes || 0;
var medium = test.slow() / 2;
test.speed = test.duration > test.slow()
? 'slow'
: test.duration > medium
? 'medium'
: 'fast';
stats.passes++;
});
runner.on('fail', function(test, err){
stats.failures = stats.failures || 0;
stats.failures++;
test.err = err;
failures.push(test);
});
runner.on('end', function(){
stats.end = new Date;
stats.duration = new Date - stats.start;
});
runner.on('pending', function(){
stats.pending++;
});
}
/*
This reporter wraps the original html reporter plus reports plain text into a hidden div.
This allows the webdriver client to pick up the test results
*/
var WebdriverAndHtmlReporter = function(html_reporter){
return function(runner){
Base.call(this, runner);
// Scroll down test display after each test
mocha = $('#mocha')[0];
runner.on('test', function(){
mocha.scrollTop = mocha.scrollHeight;
});
//initalize the html reporter first
html_reporter(runner);
var $console = $("#console");
var level = 0;
var append = function(){
var text = Array.prototype.join.apply(arguments, [" "]);
var oldText = $console.text();
var space = "";
for(var i=0;i<level*2;i++){
space+=" ";
}
var splitedText = "";
_(text.split("\n")).each(function(line){
while(line.length > 0){
var split = line.substr(0,100);
line = line.substr(100);
if(splitedText.length > 0) splitedText+="\n";
splitedText += split;
}
});
//indent all lines with the given amount of space
var newText = _(splitedText.split("\n")).map(function(line){
return space + line;
}).join("\\n");
$console.text(oldText + newText + "\\n");
}
runner.on('suite', function(suite){
if (suite.root) return;
append(suite.title);
level++;
});
runner.on('suite end', function(suite){
if (suite.root) return;
level--;
if(level == 0) {
append("");
}
});
var stringifyException = function(exception){
var err = exception.stack || exception.toString();
// FF / Opera do not add the message
if (!~err.indexOf(exception.message)) {
err = exception.message + '\n' + err;
}
// <=IE7 stringifies to [Object Error]. Since it can be overloaded, we
// check for the result of the stringifying.
if ('[object Error]' == err) err = exception.message;
// Safari doesn't give you a stack. Let's at least provide a source line.
if (!exception.stack && exception.sourceURL && exception.line !== undefined) {
err += "\n(" + exception.sourceURL + ":" + exception.line + ")";
}
return err;
}
var killTimeout;
runner.on('test end', function(test){
if ('passed' == test.state) {
append("->","[green]PASSED[clear] :", test.title);
} else if (test.pending) {
append("->","[yellow]PENDING[clear]:", test.title);
} else {
append("->","[red]FAILED[clear] :", test.title, stringifyException(test.err));
}
if(killTimeout) clearTimeout(killTimeout);
killTimeout = setTimeout(function(){
append("FINISHED - [red]no test started since 3 minutes, tests stopped[clear]");
}, 60000 * 3);
});
var total = runner.total;
runner.on('end', function(){
if(stats.tests >= total){
var minutes = Math.floor(stats.duration / 1000 / 60);
var seconds = Math.round((stats.duration / 1000) % 60);
append("FINISHED -", stats.passes, "tests passed,", stats.failures, "tests failed, duration: " + minutes + ":" + seconds);
}
});
}
}
//allow cross iframe access
if ((!$.browser.msie) && (!($.browser.mozilla && $.browser.version.indexOf("1.8.") == 0))) {
document.domain = document.domain; // for comet
}
//http://stackoverflow.com/questions/1403888/get-url-parameter-with-jquery
var getURLParameter = function (name) {
return decodeURI(
(RegExp(name + '=' + '(.+?)(&|$)').exec(location.search)||[,null])[1]
);
}
//get the list of specs and filter it if requested
var specs = specs_list.slice();
//inject spec scripts into the dom
var $body = $('body');
$.each(specs, function(i, spec){
$body.append('<script src="specs/' + spec + '"></script>')
});
//initalize the test helper
helper.init(function(){
//configure and start the test framework
var grep = getURLParameter("grep");
if(grep != "null"){
mocha.grep(grep);
}
mocha.ignoreLeaks();
mocha.reporter(WebdriverAndHtmlReporter(mocha._reporter));
mocha.run();
});
});

View file

@ -0,0 +1,36 @@
describe("bold button", function(){
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
it("makes text bold", function(done) {
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
//get the first text element out of the inner iframe
var $firstTextElement = inner$("div").first();
//select this text element
$firstTextElement.sendkeys('{selectall}');
//get the bold button and click it
var $boldButton = chrome$(".buttonicon-bold");
$boldButton.click();
//ace creates a new dom element when you press a button, so just get the first text element again
var $newFirstTextElement = inner$("div").first();
// is there a <b> element now?
var isBold = $newFirstTextElement.find("b").length === 1;
//expect it to be bold
expect(isBold).to.be(true);
//make sure the text hasn't changed
expect($newFirstTextElement.text()).to.eql($firstTextElement.text());
done();
});
});

View file

@ -0,0 +1,54 @@
describe("clear authorship colors button", function(){
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
it("makes text clear authorship colors", function(done) {
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
// override the confirm dialogue functioon
helper.padChrome$.window.confirm = function(){
return true;
}
//get the first text element out of the inner iframe
var $firstTextElement = inner$("div").first();
// Get the original text
var originalText = inner$("div").first().text();
// Set some new text
var sentText = "Hello";
//select this text element
$firstTextElement.sendkeys('{selectall}');
$firstTextElement.sendkeys(sentText);
$firstTextElement.sendkeys('{rightarrow}');
helper.waitFor(function(){
return inner$("div span").first().attr("class").indexOf("author") !== -1; // wait until we have the full value available
}).done(function(){
//IE hates you if you don't give focus to the inner frame bevore you do a clearAuthorship
inner$("div").first().focus();
//get the clear authorship colors button and click it
var $clearauthorshipcolorsButton = chrome$(".buttonicon-clearauthorship");
$clearauthorshipcolorsButton.click();
// does the first divs span include an author class?
console.log(inner$("div span").first().attr("class"));
var hasAuthorClass = inner$("div span").first().attr("class").indexOf("author") !== -1;
//expect(hasAuthorClass).to.be(false);
// does the first div include an author class?
var hasAuthorClass = inner$("div").first().attr("class").indexOf("author") !== -1;
expect(hasAuthorClass).to.be(false);
done();
});
});
});

View file

@ -0,0 +1,179 @@
describe("indentation button", function(){
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
it("indent text", function(done){
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
var $indentButton = chrome$(".buttonicon-indent");
$indentButton.click();
helper.waitFor(function(){
return inner$("div").first().find("ul li").length === 1;
}).done(done);
});
it("keeps the indent on enter for the new line", function(done){
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
var $indentButton = chrome$(".buttonicon-indent");
$indentButton.click();
//type a bit, make a line break and type again
var $firstTextElement = inner$("div span").first();
$firstTextElement.sendkeys('line 1');
$firstTextElement.sendkeys('{enter}');
$firstTextElement.sendkeys('line 2');
$firstTextElement.sendkeys('{enter}');
helper.waitFor(function(){
return inner$("div span").first().text().indexOf("line 2") === -1;
}).done(function(){
var $newSecondLine = inner$("div").first().next();
var hasULElement = $newSecondLine.find("ul li").length === 1;
expect(hasULElement).to.be(true);
expect($newSecondLine.text()).to.be("line 2");
done();
});
});
/*
it("makes text indented and outdented", function() {
//get the inner iframe
var $inner = testHelper.$getPadInner();
//get the first text element out of the inner iframe
var firstTextElement = $inner.find("div").first();
//select this text element
testHelper.selectText(firstTextElement[0], $inner);
//get the indentation button and click it
var $indentButton = testHelper.$getPadChrome().find(".buttonicon-indent");
$indentButton.click();
//ace creates a new dom element when you press a button, so just get the first text element again
var newFirstTextElement = $inner.find("div").first();
// is there a list-indent class element now?
var firstChild = newFirstTextElement.children(":first");
var isUL = firstChild.is('ul');
//expect it to be the beginning of a list
expect(isUL).to.be(true);
var secondChild = firstChild.children(":first");
var isLI = secondChild.is('li');
//expect it to be part of a list
expect(isLI).to.be(true);
//indent again
$indentButton.click();
var newFirstTextElement = $inner.find("div").first();
// is there a list-indent class element now?
var firstChild = newFirstTextElement.children(":first");
var hasListIndent2 = firstChild.hasClass('list-indent2');
//expect it to be part of a list
expect(hasListIndent2).to.be(true);
//make sure the text hasn't changed
expect(newFirstTextElement.text()).to.eql(firstTextElement.text());
// test outdent
//get the unindentation button and click it twice
var $outdentButton = testHelper.$getPadChrome().find(".buttonicon-outdent");
$outdentButton.click();
$outdentButton.click();
//ace creates a new dom element when you press a button, so just get the first text element again
var newFirstTextElement = $inner.find("div").first();
// is there a list-indent class element now?
var firstChild = newFirstTextElement.children(":first");
var isUL = firstChild.is('ul');
//expect it not to be the beginning of a list
expect(isUL).to.be(false);
var secondChild = firstChild.children(":first");
var isLI = secondChild.is('li');
//expect it to not be part of a list
expect(isLI).to.be(false);
//make sure the text hasn't changed
expect(newFirstTextElement.text()).to.eql(firstTextElement.text());
// Next test tests multiple line indentation
//select this text element
testHelper.selectText(firstTextElement[0], $inner);
//indent twice
$indentButton.click();
$indentButton.click();
//get the first text element out of the inner iframe
var firstTextElement = $inner.find("div").first();
//select this text element
testHelper.selectText(firstTextElement[0], $inner);
/* this test creates the below content, both should have double indentation
line1
line2
firstTextElement.sendkeys('{rightarrow}'); // simulate a keypress of enter
firstTextElement.sendkeys('{enter}'); // simulate a keypress of enter
firstTextElement.sendkeys('line 1'); // simulate writing the first line
firstTextElement.sendkeys('{enter}'); // simulate a keypress of enter
firstTextElement.sendkeys('line 2'); // simulate writing the second line
//get the second text element out of the inner iframe
setTimeout(function(){ // THIS IS REALLY BAD
var secondTextElement = $('iframe').contents().find('iframe').contents().find('iframe').contents().find('body > div').get(1); // THIS IS UGLY
// is there a list-indent class element now?
var firstChild = secondTextElement.children(":first");
var isUL = firstChild.is('ul');
//expect it to be the beginning of a list
expect(isUL).to.be(true);
var secondChild = secondChild.children(":first");
var isLI = secondChild.is('li');
//expect it to be part of a list
expect(isLI).to.be(true);
//get the first text element out of the inner iframe
var thirdTextElement = $('iframe').contents().find('iframe').contents().find('iframe').contents().find('body > div').get(2); // THIS IS UGLY TOO
// is there a list-indent class element now?
var firstChild = thirdTextElement.children(":first");
var isUL = firstChild.is('ul');
//expect it to be the beginning of a list
expect(isUL).to.be(true);
var secondChild = firstChild.children(":first");
var isLI = secondChild.is('li');
//expect it to be part of a list
expect(isLI).to.be(true);
},1000);
});*/
});

View file

@ -0,0 +1,36 @@
describe("italic button", function(){
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
it("makes text italic", function(done) {
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
//get the first text element out of the inner iframe
var $firstTextElement = inner$("div").first();
//select this text element
$firstTextElement.sendkeys('{selectall}');
//get the bold button and click it
var $boldButton = chrome$(".buttonicon-italic");
$boldButton.click();
//ace creates a new dom element when you press a button, so just get the first text element again
var $newFirstTextElement = inner$("div").first();
// is there a <i> element now?
var isItalic = $newFirstTextElement.find("i").length === 1;
//expect it to be bold
expect(isItalic).to.be(true);
//make sure the text hasn't changed
expect($newFirstTextElement.text()).to.eql($firstTextElement.text());
done();
});
});

View file

@ -0,0 +1,47 @@
describe("assign ordered list", function(){
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
it("insert ordered list text", function(done){
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
var $insertorderedlistButton = chrome$(".buttonicon-insertorderedlist");
$insertorderedlistButton.click();
helper.waitFor(function(){
return inner$("div").first().find("ol li").length === 1;
}).done(done);
});
xit("issue #1125 keeps the numbered list on enter for the new line - EMULATES PASTING INTO A PAD", function(done){
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
var $insertorderedlistButton = chrome$(".buttonicon-insertorderedlist");
$insertorderedlistButton.click();
//type a bit, make a line break and type again
var $firstTextElement = inner$("div span").first();
$firstTextElement.sendkeys('line 1');
$firstTextElement.sendkeys('{enter}');
$firstTextElement.sendkeys('line 2');
$firstTextElement.sendkeys('{enter}');
helper.waitFor(function(){
return inner$("div span").first().text().indexOf("line 2") === -1;
}).done(function(){
var $newSecondLine = inner$("div").first().next();
var hasOLElement = $newSecondLine.find("ol li").length === 1;
console.log($newSecondLine.find("ol"));
expect(hasOLElement).to.be(true);
expect($newSecondLine.text()).to.be("line 2");
var hasLineNumber = $newSecondLine.find("ol").attr("start") === 2;
expect(hasLineNumber).to.be(true); // This doesn't work because pasting in content doesn't work
done();
});
});
});

View file

@ -0,0 +1,37 @@
describe("undo button then redo button", function(){
beforeEach(function(cb){
helper.newPad(cb); // creates a new pad
this.timeout(60000);
});
it("undo some typing", function(done){
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
// get the first text element inside the editable space
var $firstTextElement = inner$("div span").first();
var originalValue = $firstTextElement.text(); // get the original value
var newString = "Foo";
$firstTextElement.sendkeys(newString); // send line 1 to the pad
var modifiedValue = $firstTextElement.text(); // get the modified value
expect(modifiedValue).not.to.be(originalValue); // expect the value to change
// get undo and redo buttons
var $undoButton = chrome$(".buttonicon-undo");
var $redoButton = chrome$(".buttonicon-redo");
// click the buttons
$undoButton.click(); // removes foo
$redoButton.click(); // resends foo
helper.waitFor(function(){
console.log(inner$("div span").first().text());
return inner$("div span").first().text() === newString;
}).done(function(){
var finalValue = inner$("div").first().text();
expect(finalValue).to.be(modifiedValue); // expect the value to change
done();
});
});
});

View file

@ -0,0 +1,36 @@
describe("strikethrough button", function(){
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
it("makes text strikethrough", function(done) {
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
//get the first text element out of the inner iframe
var $firstTextElement = inner$("div").first();
//select this text element
$firstTextElement.sendkeys('{selectall}');
//get the strikethrough button and click it
var $strikethroughButton = chrome$(".buttonicon-strikethrough");
$strikethroughButton.click();
//ace creates a new dom element when you press a button, so just get the first text element again
var $newFirstTextElement = inner$("div").first();
// is there a <i> element now?
var isstrikethrough = $newFirstTextElement.find("s").length === 1;
//expect it to be strikethrough
expect(isstrikethrough).to.be(true);
//make sure the text hasn't changed
expect($newFirstTextElement.text()).to.eql($firstTextElement.text());
done();
});
});

View file

@ -0,0 +1,47 @@
//deactivated, we need a nice way to get the timeslider, this is ugly
xdescribe("timeslider button takes you to the timeslider of a pad", function(){
beforeEach(function(cb){
helper.newPad(cb); // creates a new pad
this.timeout(60000);
});
it("timeslider contained in URL", function(done){
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
// get the first text element inside the editable space
var $firstTextElement = inner$("div span").first();
var originalValue = $firstTextElement.text(); // get the original value
var newValue = "Testing"+originalValue;
$firstTextElement.sendkeys("Testing"); // send line 1 to the pad
var modifiedValue = $firstTextElement.text(); // get the modified value
expect(modifiedValue).not.to.be(originalValue); // expect the value to change
helper.waitFor(function(){
return modifiedValue !== originalValue; // The value has changed so we can..
}).done(function(){
var $timesliderButton = chrome$("#timesliderlink");
$timesliderButton.click(); // So click the timeslider link
helper.waitFor(function(){
var iFrameURL = chrome$.window.location.href;
if(iFrameURL){
return iFrameURL.indexOf("timeslider") !== -1;
}else{
return false; // the URL hasnt been set yet
}
}).done(function(){
// click the buttons
var iFrameURL = chrome$.window.location.href; // get the url
var inTimeslider = iFrameURL.indexOf("timeslider") !== -1;
expect(inTimeslider).to.be(true); // expect the value to change
done();
});
});
});
});

View file

@ -0,0 +1,33 @@
describe("undo button", function(){
beforeEach(function(cb){
helper.newPad(cb); // creates a new pad
this.timeout(60000);
});
it("undo some typing", function(done){
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
// get the first text element inside the editable space
var $firstTextElement = inner$("div span").first();
var originalValue = $firstTextElement.text(); // get the original value
$firstTextElement.sendkeys("foo"); // send line 1 to the pad
var modifiedValue = $firstTextElement.text(); // get the modified value
expect(modifiedValue).not.to.be(originalValue); // expect the value to change
// get clear authorship button as a variable
var $undoButton = chrome$(".buttonicon-undo");
// click the button
$undoButton.click();
helper.waitFor(function(){
return inner$("div span").first().text() === originalValue;
}).done(function(){
var finalValue = inner$("div span").first().text();
expect(finalValue).to.be(originalValue); // expect the value to change
done();
});
});
});

View file

@ -0,0 +1,72 @@
describe("change username value", function(){
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
it("Remembers the user name after a refresh", function(done) {
this.timeout(60000);
var chrome$ = helper.padChrome$;
//click on the settings button to make settings visible
var $userButton = chrome$(".buttonicon-showusers");
$userButton.click();
var $usernameInput = chrome$("#myusernameedit");
$usernameInput.click();
$usernameInput.val('John McLear');
$usernameInput.blur();
setTimeout(function(){ //give it a second to save the username on the server side
helper.newPad({ // get a new pad, but don't clear the cookies
clearCookies: false
, cb: function(){
var chrome$ = helper.padChrome$;
//click on the settings button to make settings visible
var $userButton = chrome$(".buttonicon-showusers");
$userButton.click();
var $usernameInput = chrome$("#myusernameedit");
expect($usernameInput.val()).to.be('John McLear')
done();
}
});
}, 1000);
});
it("Own user name is shown when you enter a chat", function(done) {
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
//click on the settings button to make settings visible
var $userButton = chrome$(".buttonicon-showusers");
$userButton.click();
var $usernameInput = chrome$("#myusernameedit");
$usernameInput.click();
$usernameInput.val('John McLear');
$usernameInput.blur();
//click on the chat button to make chat visible
var $chatButton = chrome$("#chaticon");
$chatButton.click();
var $chatInput = chrome$("#chatinput");
$chatInput.sendkeys('O hi'); // simulate a keypress of typing JohnMcLear
$chatInput.sendkeys('{enter}'); // simulate a keypress of enter actually does evt.which = 10 not 13
//check if chat shows up
helper.waitFor(function(){
return chrome$("#chattext").children("p").length !== 0; // wait until the chat message shows up
}).done(function(){
var $firstChatMessage = chrome$("#chattext").children("p");
var containsJohnMcLear = $firstChatMessage.text().indexOf("John McLear") !== -1; // does the string contain John McLear
expect(containsJohnMcLear).to.be(true); // expect the first chat message to contain JohnMcLear
done();
});
});
});

View file

@ -0,0 +1,40 @@
describe("chat always ons creen select", function(){
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
it("makes chat stick to right side of the screen", function(done) {
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
//click on the settings button to make settings visible
var $settingsButton = chrome$(".buttonicon-settings");
$settingsButton.click();
//get the chat selector
var $stickychatCheckbox = chrome$("#options-stickychat");
//select chat always on screen and fire change event
$stickychatCheckbox.attr('selected','selected');
$stickychatCheckbox.change();
$stickychatCheckbox.click();
//check if chat changed to get the stickychat Class
var $chatbox = chrome$("#chatbox");
var hasStickyChatClass = $chatbox.hasClass("stickyChat");
expect(hasStickyChatClass).to.be(true);
//select chat always on screen and fire change event
$stickychatCheckbox.attr('selected','selected');
$stickychatCheckbox.change();
$stickychatCheckbox.click();
//check if chat changed to remove the stickychat Class
var hasStickyChatClass = $chatbox.hasClass("stickyChat");
expect(hasStickyChatClass).to.be(false);
done();
});
});

View file

@ -0,0 +1,133 @@
describe("embed links", function(){
var objectify = function (str)
{
var hash = {};
var parts = str.split('&');
for(var i = 0; i < parts.length; i++)
{
var keyValue = parts[i].split('=');
hash[keyValue[0]] = keyValue[1];
}
return hash;
}
var checkiFrameCode = function(embedCode, readonly){
//turn the code into an html element
var $embediFrame = $(embedCode);
//read and check the frame attributes
var width = $embediFrame.attr("width");
var height = $embediFrame.attr("height");
var name = $embediFrame.attr("name");
expect(width).to.be('600');
expect(height).to.be('400');
expect(name).to.be(readonly ? "embed_readonly" : "embed_readwrite");
//parse the url
var src = $embediFrame.attr("src");
var questionMark = src.indexOf("?");
var url = src.substr(0,questionMark);
var paramsStr = src.substr(questionMark+1);
var params = objectify(paramsStr);
var expectedParams = {
showControls: 'true'
, showChat: 'true'
, showLineNumbers: 'true'
, useMonospaceFont: 'false'
}
//check the url
if(readonly){
expect(url.indexOf("r.") > 0).to.be(true);
} else {
expect(url).to.be(helper.padChrome$.window.location.href);
}
//check if all parts of the url are like expected
expect(params).to.eql(expectedParams);
}
describe("read and write", function(){
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
describe("the share link", function(){
it("is the actual pad url", function(done){
var chrome$ = helper.padChrome$;
//open share dropdown
chrome$(".buttonicon-embed").click();
//get the link of the share field + the actual pad url and compare them
var shareLink = chrome$("#linkinput").val();
var padURL = chrome$.window.location.href;
expect(shareLink).to.be(padURL);
done();
});
});
describe("the embed as iframe code", function(){
it("is an iframe with the the correct url parameters and correct size", function(done){
var chrome$ = helper.padChrome$;
//open share dropdown
chrome$(".buttonicon-embed").click();
//get the link of the share field + the actual pad url and compare them
var embedCode = chrome$("#embedinput").val();
checkiFrameCode(embedCode, false)
done();
});
});
});
describe("when read only option is set", function(){
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
describe("the share link", function(){
it("shows a read only url", function(done){
var chrome$ = helper.padChrome$;
//open share dropdown
chrome$(".buttonicon-embed").click();
//check read only checkbox, a bit hacky
chrome$('#readonlyinput').attr('checked','checked').click().attr('checked','checked');
//get the link of the share field + the actual pad url and compare them
var shareLink = chrome$("#linkinput").val();
var containsReadOnlyLink = shareLink.indexOf("r.") > 0
expect(containsReadOnlyLink).to.be(true);
done();
});
});
describe("the embed as iframe code", function(){
it("is an iframe with the the correct url parameters and correct size", function(done){
var chrome$ = helper.padChrome$;
//open share dropdown
chrome$(".buttonicon-embed").click();
//check read only checkbox, a bit hacky
chrome$('#readonlyinput').attr('checked','checked').click().attr('checked','checked');
//get the link of the share field + the actual pad url and compare them
var embedCode = chrome$("#embedinput").val();
checkiFrameCode(embedCode, true);
done();
});
});
});
});

View file

@ -0,0 +1,30 @@
describe("font select", function(){
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
it("makes text monospace", function(done) {
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
//click on the settings button to make settings visible
var $settingsButton = chrome$(".buttonicon-settings");
$settingsButton.click();
//get the font menu and monospace option
var $viewfontmenu = chrome$("#viewfontmenu");
var $monospaceoption = $viewfontmenu.find("[value=monospace]");
//select monospace and fire change event
$monospaceoption.attr('selected','selected');
$viewfontmenu.change();
//check if font changed to monospace
var fontFamily = inner$("body").css("font-family").toLowerCase();
expect(fontFamily).to.be("monospace");
done();
});
});

View file

@ -0,0 +1,99 @@
describe("the test helper", function(){
describe("the newPad method", function(){
xit("doesn't leak memory if you creates iframes over and over again", function(done){
this.timeout(100000);
var times = 10;
var loadPad = function(){
helper.newPad(function(){
times--;
if(times > 0){
loadPad();
} else {
done();
}
})
}
loadPad();
});
it("gives me 3 jquery instances of chrome, outer and inner", function(done){
this.timeout(5000);
helper.newPad(function(){
//check if the jquery selectors have the desired elements
expect(helper.padChrome$("#editbar").length).to.be(1);
expect(helper.padOuter$("#outerdocbody").length).to.be(1);
expect(helper.padInner$("#innerdocbody").length).to.be(1);
//check if the document object was set correctly
expect(helper.padChrome$.window.document).to.be(helper.padChrome$.document);
expect(helper.padOuter$.window.document).to.be(helper.padOuter$.document);
expect(helper.padInner$.window.document).to.be(helper.padInner$.document);
done();
});
});
});
describe("the waitFor method", function(){
it("takes a timeout and waits long enough", function(done){
this.timeout(2000);
var startTime = new Date().getTime();
helper.waitFor(function(){
return false;
}, 1500).fail(function(){
var duration = new Date().getTime() - startTime;
expect(duration).to.be.greaterThan(1400);
done();
});
});
it("takes an interval and checks on every interval", function(done){
this.timeout(4000);
var checks = 0;
helper.waitFor(function(){
checks++;
return false;
}, 2000, 100).fail(function(){
expect(checks).to.be.greaterThan(10);
expect(checks).to.be.lessThan(30);
done();
});
});
describe("returns a deferred object", function(){
it("it calls done after success", function(done){
helper.waitFor(function(){
return true;
}).done(function(){
done();
});
});
it("calls fail after failure", function(done){
helper.waitFor(function(){
return false;
},0).fail(function(){
done();
});
});
xit("throws if you don't listen for fails", function(done){
var onerror = window.onerror;
window.onerror = function(){
window.onerror = onerror;
done();
}
helper.waitFor(function(){
return false;
},100);
});
});
});
});

View file

@ -0,0 +1,39 @@
describe("send chat message", function(){
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
it("opens chat, sends a message and makes sure it exists on the page", function(done) {
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
var chatValue = "JohnMcLear";
//click on the chat button to make chat visible
var $chatButton = chrome$("#chaticon");
$chatButton.click();
var $chatInput = chrome$("#chatinput");
$chatInput.sendkeys('JohnMcLear'); // simulate a keypress of typing JohnMcLear
$chatInput.sendkeys('{enter}'); // simulate a keypress of enter actually does evt.which = 10 not 13
//check if chat shows up
helper.waitFor(function(){
return chrome$("#chattext").children("p").length !== 0; // wait until the chat message shows up
}).done(function(){
var $firstChatMessage = chrome$("#chattext").children("p");
var containsMessage = $firstChatMessage.text().indexOf("JohnMcLear") !== -1; // does the string contain JohnMcLear?
expect(containsMessage).to.be(true); // expect the first chat message to contain JohnMcLear
// do a slightly more thorough check
var username = $firstChatMessage.children("b");
var usernameValue = username.text();
var time = $firstChatMessage.children(".time");
var timeValue = time.text();
var expectedStringIncludingUserNameAndTime = usernameValue + timeValue + " " + "JohnMcLear";
expect(expectedStringIncludingUserNameAndTime).to.be($firstChatMessage.text());
done();
});
});
});

View file

@ -0,0 +1,37 @@
describe("delete keystroke", function(){
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
it("makes text delete", function(done) {
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
//get the first text element out of the inner iframe
var $firstTextElement = inner$("div").first();
// get the original length of this element
var elementLength = $firstTextElement.text().length;
// get the original string value minus the last char
var originalTextValue = $firstTextElement.text();
originalTextValueMinusFirstChar = originalTextValue.substring(1, originalTextValue.length );
// simulate key presses to delete content
$firstTextElement.sendkeys('{leftarrow}'); // simulate a keypress of the left arrow key
$firstTextElement.sendkeys('{del}'); // simulate a keypress of delete
//ace creates a new dom element when you press a keystroke, so just get the first text element again
var $newFirstTextElement = inner$("div").first();
// get the new length of this element
var newElementLength = $newFirstTextElement.text().length;
//expect it to be one char less in length
expect(newElementLength).to.be((elementLength-1));
done();
});
});

View file

@ -0,0 +1,34 @@
describe("enter keystroke", function(){
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
it("creates a enw line & puts cursor onto a new line", function(done) {
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
//get the first text element out of the inner iframe
var $firstTextElement = inner$("div").first();
// get the original string value minus the last char
var originalTextValue = $firstTextElement.text();
// simulate key presses to enter content
$firstTextElement.sendkeys('{enter}');
//ace creates a new dom element when you press a keystroke, so just get the first text element again
var $newFirstTextElement = inner$("div").first();
helper.waitFor(function(){
return inner$("div").first().text() === "";
}).done(function(){
var $newSecondLine = inner$("div").first().next();
var newFirstTextElementValue = inner$("div").first().text();
expect(newFirstTextElementValue).to.be(""); // expect the first line to be blank
expect($newSecondLine.text()).to.be(originalTextValue); // expect the second line to be the same as the original first line.
done();
});
});
});

View file

@ -0,0 +1,24 @@
describe("urls", function(){
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
it("when you enter an url, it becomes clickable", function(done) {
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
//get the first text element out of the inner iframe
var firstTextElement = inner$("div").first();
// simulate key presses to delete content
firstTextElement.sendkeys('{selectall}'); // select all
firstTextElement.sendkeys('{del}'); // clear the first line
firstTextElement.sendkeys('http://etherpad.org'); // insert a URL
helper.waitFor(function(){
return inner$("div").first().find("a").length === 1;
}, 2000).done(done);
});
});

View file

@ -0,0 +1,81 @@
describe("Language select and change", function(){
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
it("makes text german", function(done) {
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
//click on the settings button to make settings visible
var $settingsButton = chrome$(".buttonicon-settings");
$settingsButton.click();
//click the language button
var $language = chrome$("#languagemenu");
var $languageoption = $language.find("[value=de]");
//select german
$languageoption.attr('selected','selected');
$language.change();
var localizedEventFired = false;
$(chrome$.window).bind('localized', function() {
localizedEventFired = true;
})
helper.waitFor(function() { return localizedEventFired;})
.done(function(){
//get the value of the bold button
var $boldButton = chrome$(".buttonicon-bold").parent();
//get the title of the bold button
var boldButtonTitle = $boldButton[0]["title"];
//check if the language is now german
expect(boldButtonTitle).to.be("Fett (Strg-B)");
done();
});
});
it("makes text English", function(done) {
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
//click on the settings button to make settings visible
var $settingsButton = chrome$(".buttonicon-settings");
$settingsButton.click();
//click the language button
var $language = chrome$("#languagemenu");
var $languageoption = $language.find("[value=en]");
//select german
$languageoption.attr('selected','selected');
$language.change();
var localizedEventFired = false;
$(chrome$.window).bind('localized', function() {
localizedEventFired = true;
})
helper.waitFor(function() { return localizedEventFired;})
.done(function(){
//get the value of the bold button
var $boldButton = chrome$(".buttonicon-bold").parent();
//get the title of the bold button
var boldButtonTitle = $boldButton[0]["title"];
//check if the language is now English
expect(boldButtonTitle).to.be("Bold (Ctrl-B)");
done();
});
});
});

2
tests/frontend/travis/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
sauce_connect.log
sauce_connect.log.*

View file

@ -0,0 +1,110 @@
var srcFolder = "../../../src/node_modules/";
var wd = require(srcFolder + "wd");
var async = require(srcFolder + "async");
var config = {
host: "ondemand.saucelabs.com"
, port: 80
, username: process.env.SAUCE_USER
, accessKey: process.env.SAUCE_KEY
}
var allTestsPassed = true;
var sauceTestWorker = async.queue(function (testSettings, callback) {
var browser = wd.remote(config.host, config.port, config.username, config.accessKey);
var browserChain = browser.chain();
var name = process.env.GIT_HASH + " - " + testSettings.browserName + " " + testSettings.version + ", " + testSettings.platform;
testSettings.name = name;
testSettings["public"] = true;
testSettings["build"] = process.env.GIT_HASH;
browserChain.init(testSettings).get("http://localhost:9001/tests/frontend/", function(){
var url = "https://saucelabs.com/jobs/" + browser.sessionID;
console.log("Remote sauce test '" + name + "' started! " + url);
//tear down the test excecution
var stopSauce = function(success){
getStatusInterval && clearInterval(getStatusInterval);
clearTimeout(timeout);
browserChain.quit();
if(!success){
allTestsPassed = false;
}
var testResult = knownConsoleText.replace(/\[red\]/g,'\x1B[31m').replace(/\[yellow\]/g,'\x1B[33m')
.replace(/\[green\]/g,'\x1B[32m').replace(/\[clear\]/g, '\x1B[39m');
testResult = testResult.split("\\n").map(function(line){
return "[" + testSettings.browserName + (testSettings.version === "" ? '' : (" " + testSettings.version)) + "] " + line;
}).join("\n");
console.log(testResult);
console.log("Remote sauce test '" + name + "' finished! " + url);
callback();
}
//timeout for the case the test hangs
var timeout = setTimeout(function(){
stopSauce(false);
}, 60000 * 10);
var knownConsoleText = "";
var getStatusInterval = setInterval(function(){
browserChain.eval("$('#console').text()", function(err, consoleText){
if(!consoleText || err){
return;
}
knownConsoleText = consoleText;
if(knownConsoleText.indexOf("FINISHED") > 0){
var success = knownConsoleText.indexOf("FAILED") === -1;
stopSauce(success);
}
});
}, 5000);
});
}, 5); //run 5 tests in parrallel
// Firefox
sauceTestWorker.push({
'platform' : 'Linux'
, 'browserName' : 'firefox'
, 'version' : ''
});
// Chrome
sauceTestWorker.push({
'platform' : 'Linux'
, 'browserName' : 'googlechrome'
, 'version' : ''
});
// IE 8
sauceTestWorker.push({
'platform' : 'Windows 2003'
, 'browserName' : 'iexplore'
, 'version' : '8'
});
// IE 9
sauceTestWorker.push({
'platform' : 'Windows 2008'
, 'browserName' : 'iexplore'
, 'version' : '9'
});
// IE 10
sauceTestWorker.push({
'platform' : 'Windows 2012'
, 'browserName' : 'iexplore'
, 'version' : '10'
});
sauceTestWorker.drain = function() {
setTimeout(function(){
process.exit(allTestsPassed ? 0 : 1);
}, 3000);
}

18
tests/frontend/travis/runner.sh Executable file
View file

@ -0,0 +1,18 @@
#!/bin/sh
#Move to the base folder
cd `dirname $0`
#start etherpad lite
../../../bin/run.sh > /dev/null &
sleep 10
#start remote runner
node remote_runner.js
exit_code=$?
kill $!
kill $(cat /tmp/sauce.pid)
sleep 30
exit $exit_code

View file

@ -0,0 +1,16 @@
#!/bin/bash
# download and unzip the sauce connector
curl http://saucelabs.com/downloads/Sauce-Connect-latest.zip > /tmp/sauce.zip
unzip /tmp/sauce.zip -d /tmp
# start the sauce connector in background and make sure it doesn't output the secret key
(java -jar /tmp/Sauce-Connect.jar $SAUCE_USER $SAUCE_KEY -f /tmp/tunnel > /dev/null )&
# save the sauce pid in a file
echo $! > /tmp/sauce.pid
# wait for the tunnel to build up
while [ ! -e "/tmp/tunnel" ]
do
sleep 1
done