diff options
author | 0ip <me@factor.cc> | 2012-04-09 15:10:08 +0200 |
---|---|---|
committer | 0ip <me@factor.cc> | 2012-04-09 15:10:08 +0200 |
commit | 60eea0a3cff5058656913935525bc53d368c78cf (patch) | |
tree | 64ce00064a955ca4b66cdfc2a2fd60469b030901 | |
parent | 309e3b09942389bd92205fc04d5e73088a30df60 (diff) | |
parent | 18038ddd5081e052af7ef8fa0c9677826dff61a5 (diff) | |
download | etherpad-lite-60eea0a3cff5058656913935525bc53d368c78cf.zip |
Merge branch 'develop' of git://github.com/Pita/etherpad-lite into develop
-rw-r--r-- | .gitignore | 7 | ||||
-rw-r--r-- | README.plugins | 16 | ||||
-rw-r--r-- | available_plugins/ep_fintest/.npmignore | 7 | ||||
-rw-r--r-- | available_plugins/ep_fintest/ep.json | 36 | ||||
-rw-r--r-- | available_plugins/ep_fintest/otherpart.js | 25 | ||||
-rw-r--r-- | available_plugins/ep_fintest/package.json | 9 | ||||
-rw-r--r-- | available_plugins/ep_fintest/partlast.js | 3 | ||||
-rw-r--r-- | available_plugins/ep_fintest/static/js/test.js | 5 | ||||
-rw-r--r-- | available_plugins/ep_fintest/static/test.html | 1 | ||||
-rw-r--r-- | bin/checkPad.js | 4 | ||||
-rw-r--r-- | bin/convert.js | 10 | ||||
-rwxr-xr-x | bin/debugRun.sh | 3 | ||||
-rwxr-xr-x | bin/installDeps.sh | 37 | ||||
-rwxr-xr-x | bin/run.sh | 3 | ||||
-rw-r--r-- | node/server.js | 500 | ||||
-rw-r--r-- | settings.json.template | 16 | ||||
-rw-r--r-- | settings.json.template_windows | 8 | ||||
-rw-r--r-- | src/ep.json | 16 | ||||
-rw-r--r-- | src/node/README.md (renamed from node/README.md) | 0 | ||||
-rw-r--r-- | src/node/db/API.js (renamed from node/db/API.js) | 0 | ||||
-rw-r--r-- | src/node/db/AuthorManager.js (renamed from node/db/AuthorManager.js) | 4 | ||||
-rw-r--r-- | src/node/db/DB.js (renamed from node/db/DB.js) | 0 | ||||
-rw-r--r-- | src/node/db/GroupManager.js (renamed from node/db/GroupManager.js) | 4 | ||||
-rw-r--r-- | src/node/db/Pad.js (renamed from node/db/Pad.js) | 111 | ||||
-rw-r--r-- | src/node/db/PadManager.js (renamed from node/db/PadManager.js) | 0 | ||||
-rw-r--r-- | src/node/db/ReadOnlyManager.js (renamed from node/db/ReadOnlyManager.js) | 4 | ||||
-rw-r--r-- | src/node/db/SecurityManager.js (renamed from node/db/SecurityManager.js) | 4 | ||||
-rw-r--r-- | src/node/db/SessionManager.js (renamed from node/db/SessionManager.js) | 4 | ||||
-rw-r--r-- | src/node/easysync_tests.js (renamed from node/easysync_tests.js) | 24 | ||||
-rw-r--r-- | src/node/eejs/examples/bar.ejs | 9 | ||||
-rw-r--r-- | src/node/eejs/examples/foo.ejs | 7 | ||||
-rw-r--r-- | src/node/eejs/index.js | 115 | ||||
-rw-r--r-- | src/node/handler/APIHandler.js (renamed from node/handler/APIHandler.js) | 4 | ||||
-rw-r--r-- | src/node/handler/ExportHandler.js (renamed from node/handler/ExportHandler.js) | 0 | ||||
-rw-r--r-- | src/node/handler/ImportHandler.js (renamed from node/handler/ImportHandler.js) | 0 | ||||
-rw-r--r-- | src/node/handler/PadMessageHandler.js (renamed from node/handler/PadMessageHandler.js) | 59 | ||||
-rw-r--r-- | src/node/handler/SocketIORouter.js (renamed from node/handler/SocketIORouter.js) | 0 | ||||
-rw-r--r-- | src/node/handler/TimesliderMessageHandler.js (renamed from node/handler/TimesliderMessageHandler.js) | 15 | ||||
-rw-r--r-- | src/node/hooks/express/adminplugins.js | 51 | ||||
-rw-r--r-- | src/node/hooks/express/apicalls.js | 60 | ||||
-rw-r--r-- | src/node/hooks/express/errorhandling.js | 52 | ||||
-rw-r--r-- | src/node/hooks/express/importexport.js | 41 | ||||
-rw-r--r-- | src/node/hooks/express/padreadonly.js | 65 | ||||
-rw-r--r-- | src/node/hooks/express/padurlsanitize.js | 29 | ||||
-rw-r--r-- | src/node/hooks/express/socketio.js | 49 | ||||
-rw-r--r-- | src/node/hooks/express/specialpages.js | 46 | ||||
-rw-r--r-- | src/node/hooks/express/static.js | 57 | ||||
-rw-r--r-- | src/node/hooks/express/webaccess.js | 51 | ||||
-rw-r--r-- | src/node/padaccess.js | 21 | ||||
-rw-r--r-- | src/node/server.js | 99 | ||||
-rw-r--r-- | src/node/utils/Abiword.js (renamed from node/utils/Abiword.js) | 0 | ||||
-rw-r--r-- | src/node/utils/Cli.js (renamed from node/utils/Cli.js) | 0 | ||||
-rw-r--r-- | src/node/utils/ExportDokuWiki.js (renamed from node/utils/ExportDokuWiki.js) | 4 | ||||
-rw-r--r-- | src/node/utils/ExportHtml.js (renamed from node/utils/ExportHtml.js) | 6 | ||||
-rw-r--r-- | src/node/utils/ImportHtml.js (renamed from node/utils/ImportHtml.js) | 8 | ||||
-rw-r--r-- | src/node/utils/Minify.js (renamed from node/utils/Minify.js) | 38 | ||||
-rw-r--r-- | src/node/utils/Minify.js.rej | 513 | ||||
-rw-r--r-- | src/node/utils/Settings.js (renamed from node/utils/Settings.js) | 17 | ||||
-rw-r--r-- | src/node/utils/caching_middleware.js (renamed from node/utils/caching_middleware.js) | 8 | ||||
-rw-r--r-- | src/node/utils/customError.js (renamed from node/utils/customError.js) | 0 | ||||
-rw-r--r-- | src/node/utils/randomstring.js | 16 | ||||
-rw-r--r-- | src/node/utils/tar.json (renamed from node/utils/tar.json) | 13 | ||||
-rw-r--r-- | src/package.json (renamed from package.json) | 12 | ||||
-rw-r--r-- | src/static/css/admin.css | 49 | ||||
-rw-r--r-- | src/static/css/iframe_editor.css (renamed from static/css/iframe_editor.css) | 0 | ||||
-rw-r--r-- | src/static/css/pad.css (renamed from static/css/pad.css) | 58 | ||||
-rw-r--r-- | src/static/css/timeslider.css (renamed from static/css/timeslider.css) | 68 | ||||
-rw-r--r-- | src/static/custom/.gitignore | 3 | ||||
-rw-r--r-- | src/static/custom/css.template (renamed from static/custom/css.template) | 0 | ||||
-rw-r--r-- | src/static/custom/js.template (renamed from static/custom/js.template) | 0 | ||||
-rw-r--r-- | src/static/favicon.ico (renamed from static/favicon.ico) | bin | 1150 -> 1150 bytes | |||
-rw-r--r-- | src/static/img/backgrad.gif (renamed from static/img/backgrad.gif) | bin | 697 -> 697 bytes | |||
-rw-r--r-- | src/static/img/connectingbar.gif (renamed from static/img/connectingbar.gif) | bin | 10819 -> 10819 bytes | |||
-rw-r--r-- | src/static/img/crushed_button_depressed.png (renamed from static/img/crushed_button_depressed.png) | bin | 4134 -> 4134 bytes | |||
-rw-r--r-- | src/static/img/crushed_button_undepressed.png (renamed from static/img/crushed_button_undepressed.png) | bin | 4166 -> 4166 bytes | |||
-rw-r--r-- | src/static/img/crushed_current_location.png (renamed from static/img/crushed_current_location.png) | bin | 1009 -> 1009 bytes | |||
-rw-r--r-- | src/static/img/etherpad_lite_icons.png | bin | 0 -> 8318 bytes | |||
-rw-r--r-- | src/static/img/fileicons.gif (renamed from static/img/fileicons.gif) | bin | 1649 -> 1649 bytes | |||
-rw-r--r-- | src/static/img/leftarrow.png (renamed from static/img/leftarrow.png) | bin | 494 -> 494 bytes | |||
-rw-r--r-- | src/static/img/loading.gif (renamed from static/img/loading.gif) | bin | 658 -> 658 bytes | |||
-rw-r--r-- | src/static/img/pause.png (renamed from static/img/pause.png) | bin | 2883 -> 2883 bytes | |||
-rw-r--r-- | src/static/img/play.png (renamed from static/img/play.png) | bin | 3017 -> 3017 bytes | |||
-rw-r--r-- | src/static/img/roundcorner_left.gif (renamed from static/img/roundcorner_left.gif) | bin | 123 -> 123 bytes | |||
-rw-r--r-- | src/static/img/roundcorner_right.gif (renamed from static/img/roundcorner_right.gif) | bin | 131 -> 131 bytes | |||
-rw-r--r-- | src/static/img/star.png | bin | 0 -> 3241 bytes | |||
-rw-r--r-- | src/static/img/stepper_buttons.png (renamed from static/img/stepper_buttons.png) | bin | 4858 -> 4858 bytes | |||
-rw-r--r-- | src/static/img/timeslider_background.png (renamed from static/img/timeslider_background.png) | bin | 182 -> 182 bytes | |||
-rw-r--r-- | src/static/img/timeslider_left.png (renamed from static/img/timeslider_left.png) | bin | 686 -> 686 bytes | |||
-rw-r--r-- | src/static/img/timeslider_right.png (renamed from static/img/timeslider_right.png) | bin | 517 -> 517 bytes | |||
-rw-r--r-- | src/static/js/AttributeManager.js | 164 | ||||
-rw-r--r-- | src/static/js/AttributePool.js | 96 | ||||
-rw-r--r-- | src/static/js/Changeset.js (renamed from static/js/Changeset.js) | 4 | ||||
-rw-r--r-- | src/static/js/ChangesetUtils.js | 60 | ||||
-rw-r--r-- | src/static/js/ace.js (renamed from static/js/ace.js) | 49 | ||||
-rw-r--r-- | src/static/js/ace2_common.js (renamed from static/js/ace2_common.js) | 60 | ||||
-rw-r--r-- | src/static/js/ace2_inner.js (renamed from static/js/ace2_inner.js) | 604 | ||||
-rw-r--r-- | src/static/js/broadcast.js (renamed from static/js/broadcast.js) | 28 | ||||
-rw-r--r-- | src/static/js/broadcast_revisions.js (renamed from static/js/broadcast_revisions.js) | 0 | ||||
-rw-r--r-- | src/static/js/broadcast_slider.js (renamed from static/js/broadcast_slider.js) | 61 | ||||
-rw-r--r-- | src/static/js/changesettracker.js (renamed from static/js/changesettracker.js) | 8 | ||||
-rw-r--r-- | src/static/js/chat.js (renamed from static/js/chat.js) | 13 | ||||
-rw-r--r-- | src/static/js/collab_client.js (renamed from static/js/collab_client.js) | 2 | ||||
-rw-r--r-- | src/static/js/colorutils.js (renamed from static/js/colorutils.js) | 0 | ||||
-rw-r--r-- | src/static/js/contentcollector.js (renamed from static/js/contentcollector.js) | 69 | ||||
-rw-r--r-- | src/static/js/cssmanager.js (renamed from static/js/cssmanager.js) | 0 | ||||
-rw-r--r-- | src/static/js/domline.js (renamed from static/js/domline.js) | 48 | ||||
-rw-r--r-- | src/static/js/draggable.js (renamed from static/js/draggable.js) | 0 | ||||
-rw-r--r-- | src/static/js/excanvas.js (renamed from static/js/excanvas.js) | 0 | ||||
-rw-r--r-- | src/static/js/farbtastic.js (renamed from static/js/farbtastic.js) | 0 | ||||
-rw-r--r-- | src/static/js/jquery.js | 9266 | ||||
-rw-r--r-- | src/static/js/json2.js (renamed from static/js/json2.js) | 2 | ||||
-rw-r--r-- | src/static/js/linestylefilter.js (renamed from static/js/linestylefilter.js) | 33 | ||||
-rw-r--r-- | src/static/js/pad.js (renamed from static/js/pad.js) | 92 | ||||
-rw-r--r-- | src/static/js/pad_connectionstatus.js (renamed from static/js/pad_connectionstatus.js) | 2 | ||||
-rw-r--r-- | src/static/js/pad_cookie.js (renamed from static/js/pad_cookie.js) | 0 | ||||
-rw-r--r-- | src/static/js/pad_docbar.js (renamed from static/js/pad_docbar.js) | 4 | ||||
-rw-r--r-- | src/static/js/pad_editbar.js (renamed from static/js/pad_editbar.js) | 15 | ||||
-rw-r--r-- | src/static/js/pad_editor.js (renamed from static/js/pad_editor.js) | 10 | ||||
-rw-r--r-- | src/static/js/pad_impexp.js (renamed from static/js/pad_impexp.js) | 2 | ||||
-rw-r--r-- | src/static/js/pad_modals.js (renamed from static/js/pad_modals.js) | 4 | ||||
-rw-r--r-- | src/static/js/pad_savedrevs.js (renamed from node/utils/common_code.js) | 16 | ||||
-rw-r--r-- | src/static/js/pad_userlist.js (renamed from static/js/pad_userlist.js) | 2 | ||||
-rw-r--r-- | src/static/js/pad_utils.js (renamed from static/js/pad_utils.js) | 8 | ||||
-rw-r--r-- | src/static/js/pluginfw/async.js | 690 | ||||
-rw-r--r-- | src/static/js/pluginfw/hooks.js | 90 | ||||
-rw-r--r-- | src/static/js/pluginfw/installer.js | 76 | ||||
-rw-r--r-- | src/static/js/pluginfw/parent_require.js | 37 | ||||
-rw-r--r-- | src/static/js/pluginfw/plugins.js | 226 | ||||
-rw-r--r-- | src/static/js/pluginfw/read-installed.js | 324 | ||||
-rw-r--r-- | src/static/js/pluginfw/tsort.js | 112 | ||||
-rw-r--r-- | src/static/js/rjquery.js | 5 | ||||
-rw-r--r-- | src/static/js/security.js (renamed from static/js/security.js) | 0 | ||||
-rw-r--r-- | src/static/js/skiplist.js (renamed from static/js/skiplist.js) | 41 | ||||
-rw-r--r-- | src/static/js/timeslider.js (renamed from static/js/timeslider.js) | 22 | ||||
-rw-r--r-- | src/static/js/underscore.js | 999 | ||||
-rw-r--r-- | src/static/js/undo-xpopup.js (renamed from static/js/undo-xpopup.js) | 0 | ||||
-rw-r--r-- | src/static/js/undomodule.js (renamed from static/js/undomodule.js) | 6 | ||||
-rw-r--r-- | src/static/js/virtual_lines.js (renamed from static/js/virtual_lines.js) | 0 | ||||
-rw-r--r-- | src/static/robots.txt (renamed from static/robots.txt) | 0 | ||||
-rw-r--r-- | src/static/tests.html (renamed from static/tests.html) | 0 | ||||
-rw-r--r-- | src/templates/admin/plugins.html | 168 | ||||
-rw-r--r-- | src/templates/index.html (renamed from static/index.html) | 1 | ||||
-rw-r--r-- | src/templates/pad.html | 308 | ||||
-rw-r--r-- | src/templates/timeslider.html | 151 | ||||
-rw-r--r-- | static/custom/.gitignore | 3 | ||||
-rw-r--r-- | static/img/etherpad_lite_icons.png | bin | 5541 -> 0 bytes | |||
-rw-r--r-- | static/js/AttributePoolFactory.js | 90 | ||||
-rw-r--r-- | static/js/pad_savedrevs.js | 526 | ||||
-rw-r--r-- | static/js/plugins.js | 37 | ||||
-rw-r--r-- | static/js/prefixfree.js | 421 | ||||
-rw-r--r-- | static/pad.html | 279 | ||||
-rw-r--r-- | static/timeslider.html | 218 |
152 files changed, 15117 insertions, 2939 deletions
@@ -1,7 +1,5 @@ node_modules settings.json -static/js/jquery.js -static/js/prefixfree.js APIKEY.txt bin/abiword.exe bin/node.exe @@ -10,4 +8,7 @@ var/dirty.db bin/convertSettings.json *~ *.patch -*.DS_Store
\ No newline at end of file +src/static/js/jquery.js +npm-debug.log +*.DS_Store +.ep_initialized diff --git a/README.plugins b/README.plugins new file mode 100644 index 00000000..72c45644 --- /dev/null +++ b/README.plugins @@ -0,0 +1,16 @@ +So, a plugin is an npm package whose name starts with ep_ and that contains a file ep.json +require("ep_etherpad-lite/static/js/plugingfw/plugins").update() will use npm to list all installed modules and read their ep.json files. These will contain registrations for hooks which are loaded +A hook registration is a pairs of a hook name and a function reference (filename for require() plus function name) +require("ep_etherpad-lite/static/js/plugingfw/hooks").callAll("hook_name", {argname:value}) will call all hook functions registered for hook_name +That is the basis. +Ok, so that was a slight simplification: inside ep.json, hook registrations are grouped into groups called "parts". Parts from all plugins are ordered using a topological sort according to "pre" and "post" pointers to other plugins/parts (just like dependencies, but non-installed plugins are silently ignored). +This ordering is honored when you do callAll(hook_name) - hook functions for that hook_name are called in that order +Ordering between plugins is undefined, only parts are ordered. + +A plugin usually has one part, but it van have multiple. +This is so that it can insert some hook registration before that of another plugin, and another one after. +This is important for e.g. registering URL-handlers for the express webserver, if you have some very generic and some very specific url-regexps +So, that's basically it... apart from client-side hooks +which works the same way, but uses a separate member of the part (part.client_hooks vs part.hooks), and where the hook function must obviously reside in a file require():able from the client... +One thing more: The main etherpad tree is actually a plugin itself, called ep_etherpad-lite, and it has it's own ep.json... +was that clear?
\ No newline at end of file diff --git a/available_plugins/ep_fintest/.npmignore b/available_plugins/ep_fintest/.npmignore new file mode 100644 index 00000000..74bd365b --- /dev/null +++ b/available_plugins/ep_fintest/.npmignore @@ -0,0 +1,7 @@ +.git* +docs/ +examples/ +support/ +test/ +testing.js +.DS_Store diff --git a/available_plugins/ep_fintest/ep.json b/available_plugins/ep_fintest/ep.json new file mode 100644 index 00000000..4ec8e392 --- /dev/null +++ b/available_plugins/ep_fintest/ep.json @@ -0,0 +1,36 @@ +{ + "parts": [ + { + "name": "somepart", + "pre": [], + "post": ["ep_onemoreplugin/partone"] + }, + { + "name": "partlast", + "pre": ["ep_fintest/otherpart"], + "post": [], + "hooks": { + "somehookname": "ep_fintest/partlast:somehook" + } + }, + { + "name": "partfirst", + "pre": [], + "post": ["ep_onemoreplugin/somepart"] + }, + { + "name": "otherpart", + "pre": ["ep_fintest/somepart", "ep_otherplugin/main"], + "post": [], + "hooks": { + "somehookname": "ep_fintest/otherpart:somehook", + "morehook": "ep_fintest/otherpart:morehook", + "expressCreateServer": "ep_fintest/otherpart:expressServer", + "eejsBlock_editbarMenuLeft": "ep_fintest/otherpart:eejsBlock_editbarMenuLeft" + }, + "client_hooks": { + "somehookname": "ep_fintest/static/js/test:bar" + } + } + ] +} diff --git a/available_plugins/ep_fintest/otherpart.js b/available_plugins/ep_fintest/otherpart.js new file mode 100644 index 00000000..718fb095 --- /dev/null +++ b/available_plugins/ep_fintest/otherpart.js @@ -0,0 +1,25 @@ +test = require("ep_fintest/static/js/test.js"); +console.log("FOOO:", test.foo); + +exports.somehook = function (hook_name, args, cb) { + return cb(["otherpart:somehook was here"]); +} + +exports.morehook = function (hook_name, args, cb) { + return cb(["otherpart:morehook was here"]); +} + +exports.expressServer = function (hook_name, args, cb) { + args.app.get('/otherpart', function(req, res) { + res.send("<em>Abra cadabra</em>"); + }); +} + +exports.eejsBlock_editbarMenuLeft = function (hook_name, args, cb) { + args.content = args.content + '\ + <li id="testButton" onClick="window.pad&&pad.editbarClick(\'clearauthorship\');return false;">\ + <a class="buttonicon buttonicon-test" title="Test test test"></a>\ + </li>\ + '; + return cb(); +} diff --git a/available_plugins/ep_fintest/package.json b/available_plugins/ep_fintest/package.json new file mode 100644 index 00000000..e221b5c1 --- /dev/null +++ b/available_plugins/ep_fintest/package.json @@ -0,0 +1,9 @@ +{ + "name": "ep_fintest", + "description": "A test plugin", + "version": "0.0.1", + "author": "RedHog (Egil Moeller) <egil.moller@freecode.no>", + "contributors": [], + "dependencies": {}, + "engines": { "node": ">= 0.4.1 < 0.7.0" } +} diff --git a/available_plugins/ep_fintest/partlast.js b/available_plugins/ep_fintest/partlast.js new file mode 100644 index 00000000..c3f1fc3e --- /dev/null +++ b/available_plugins/ep_fintest/partlast.js @@ -0,0 +1,3 @@ +exports.somehook = function (hook_name, args, cb) { + return cb(["partlast:somehook was here"]); +} diff --git a/available_plugins/ep_fintest/static/js/test.js b/available_plugins/ep_fintest/static/js/test.js new file mode 100644 index 00000000..22d58cc2 --- /dev/null +++ b/available_plugins/ep_fintest/static/js/test.js @@ -0,0 +1,5 @@ +exports.foo = 42; + +exports.bar = function (hook_name, args, cb) { + return cb(["FOOOO"]); +}
\ No newline at end of file diff --git a/available_plugins/ep_fintest/static/test.html b/available_plugins/ep_fintest/static/test.html new file mode 100644 index 00000000..9e7fc551 --- /dev/null +++ b/available_plugins/ep_fintest/static/test.html @@ -0,0 +1 @@ +<em>Test bla bla</em> diff --git a/bin/checkPad.js b/bin/checkPad.js index 356b0779..a72f0060 100644 --- a/bin/checkPad.js +++ b/bin/checkPad.js @@ -15,8 +15,8 @@ var log4js = require("log4js"); log4js.setGlobalLogLevel("INFO"); var async = require("async"); var db = require('../node/db/DB'); -var CommonCode = require('../node/utils/common_code'); -var Changeset = CommonCode.require("/Changeset"); + +var Changeset = require("ep_etherpad-lite/static/js/Changeset"); var padManager; async.series([ diff --git a/bin/convert.js b/bin/convert.js index c5dc535c..ec792717 100644 --- a/bin/convert.js +++ b/bin/convert.js @@ -1,12 +1,12 @@ -var CommonCode = require('../node/utils/common_code'); + var startTime = new Date().getTime(); var fs = require("fs"); var ueberDB = require("ueberDB"); var mysql = require("mysql"); var async = require("async"); -var Changeset = CommonCode.require("/Changeset"); -var randomString = CommonCode.require('/pad_utils').randomString; -var AttributePoolFactory = CommonCode.require("/AttributePoolFactory"); +var Changeset = require("ep_etherpad-lite/static/js/Changeset"); +var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; +var AttributePool = require("ep_etherpad-lite/static/js/AttributePool"); var settingsFile = process.argv[2]; var sqlOutputFile = process.argv[3]; @@ -384,7 +384,7 @@ function convertPad(padId, callback) } //generate the latest atext - var fullAPool = AttributePoolFactory.createAttributePool().fromJsonable(apool); + var fullAPool = (new AttributePool()).fromJsonable(apool); var keyRev = Math.floor(padmeta.head / padmeta.keyRevInterval) * padmeta.keyRevInterval; var atext = changesetsMeta[keyRev].atext; var curRev = keyRev; diff --git a/bin/debugRun.sh b/bin/debugRun.sh index 01197a6b..f90009d0 100755 --- a/bin/debugRun.sh +++ b/bin/debugRun.sh @@ -22,8 +22,7 @@ node-inspector & echo "If you are new to node-inspector, take a look at this video: http://youtu.be/AOnK3NVnxL8" -cd "node" -node --debug server.js +node --debug node_modules/ep_etherpad-lite/node/server.js $* #kill node-inspector before ending kill $! diff --git a/bin/installDeps.sh b/bin/installDeps.sh index 270ec98c..2acebd82 100755 --- a/bin/installDeps.sh +++ b/bin/installDeps.sh @@ -55,7 +55,13 @@ if [ ! -f $settings ]; then fi echo "Ensure that all dependencies are up to date..." -npm install || { +( + mkdir -p node_modules + cd node_modules + [ -e ep_etherpad-lite ] || ln -s ../src ep_etherpad-lite + cd ep_etherpad-lite + npm install +) || { rm -rf node_modules exit 1 } @@ -63,8 +69,8 @@ npm install || { echo "Ensure jQuery is downloaded and up to date..." DOWNLOAD_JQUERY="true" NEEDED_VERSION="1.7.1" -if [ -f "static/js/jquery.js" ]; then - VERSION=$(cat static/js/jquery.js | head -n 3 | grep -o "v[0-9]\.[0-9]\(\.[0-9]\)\?"); +if [ -f "src/static/js/jquery.js" ]; then + VERSION=$(cat src/static/js/jquery.js | head -n 3 | grep -o "v[0-9]\.[0-9]\(\.[0-9]\)\?"); if [ ${VERSION#v} = $NEEDED_VERSION ]; then DOWNLOAD_JQUERY="false" @@ -72,22 +78,7 @@ if [ -f "static/js/jquery.js" ]; then fi if [ $DOWNLOAD_JQUERY = "true" ]; then - curl -lo static/js/jquery.js http://code.jquery.com/jquery-$NEEDED_VERSION.js || exit 1 -fi - -echo "Ensure prefixfree is downloaded and up to date..." -DOWNLOAD_PREFIXFREE="true" -NEEDED_VERSION="1.0.4" -if [ -f "static/js/prefixfree.js" ]; then - VERSION=$(cat static/js/prefixfree.js | grep "PrefixFree" | grep -o "[0-9].[0-9].[0-9]"); - - if [ $VERSION = $NEEDED_VERSION ]; then - DOWNLOAD_PREFIXFREE="false" - fi -fi - -if [ $DOWNLOAD_PREFIXFREE = "true" ]; then - curl -lo static/js/prefixfree.js -k https://raw.github.com/LeaVerou/prefixfree/master/prefixfree.js || exit 1 + curl -lo src/static/js/jquery.js http://code.jquery.com/jquery-$NEEDED_VERSION.js || exit 1 fi #Remove all minified data to force node creating it new @@ -98,12 +89,12 @@ echo "ensure custom css/js files are created..." for f in "index" "pad" "timeslider" do - if [ ! -f "static/custom/$f.js" ]; then - cp -v "static/custom/js.template" "static/custom/$f.js" || exit 1 + if [ ! -f "src/static/custom/$f.js" ]; then + cp -v "src/static/custom/js.template" "src/static/custom/$f.js" || exit 1 fi - if [ ! -f "static/custom/$f.css" ]; then - cp -v "static/custom/css.template" "static/custom/$f.css" || exit 1 + if [ ! -f "src/static/custom/$f.css" ]; then + cp -v "src/static/custom/css.template" "src/static/custom/$f.css" || exit 1 fi done @@ -25,5 +25,4 @@ bin/installDeps.sh $* || exit 1 #Move to the node folder and start echo "start..." -cd "node" -node server.js $* +node node_modules/ep_etherpad-lite/node/server.js $* diff --git a/node/server.js b/node/server.js deleted file mode 100644 index c5377d81..00000000 --- a/node/server.js +++ /dev/null @@ -1,500 +0,0 @@ -/** - * This module is started with bin/run.sh. It sets up a Express HTTP and a Socket.IO Server. - * Static file Requests are answered directly from this module, Socket.IO messages are passed - * to MessageHandler and minfied requests are passed to minified. - */ - -/* - * 2011 Peter 'Pita' Martischka (Primary Technology Ltd) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var ERR = require("async-stacktrace"); -var log4js = require('log4js'); -var os = require("os"); -var socketio = require('socket.io'); -var fs = require('fs'); -var settings = require('./utils/Settings'); -var db = require('./db/DB'); -var async = require('async'); -var express = require('express'); -var path = require('path'); -var minify = require('./utils/Minify'); -var CachingMiddleware = require('./utils/caching_middleware'); -var Yajsml = require('yajsml'); -var formidable = require('formidable'); -var apiHandler; -var exportHandler; -var importHandler; -var exporthtml; -var readOnlyManager; -var padManager; -var securityManager; -var socketIORouter; - -//try to get the git version -var version = ""; -try -{ - var rootPath = path.normalize(__dirname + "/../") - var ref = fs.readFileSync(rootPath + ".git/HEAD", "utf-8"); - var refPath = rootPath + ".git/" + ref.substring(5, ref.indexOf("\n")); - version = fs.readFileSync(refPath, "utf-8"); - version = version.substring(0, 7); - console.log("Your Etherpad Lite git version is " + version); -} -catch(e) -{ - console.warn("Can't get git version for server header\n" + e.message) -} - -console.log("Report bugs at https://github.com/Pita/etherpad-lite/issues") - -var serverName = "Etherpad-Lite " + version + " (http://j.mp/ep-lite)"; - -exports.maxAge = settings.maxAge; - -//set loglevel -log4js.setGlobalLogLevel(settings.loglevel); - -async.waterfall([ - //initalize the database - function (callback) - { - db.init(callback); - }, - //initalize the http server - function (callback) - { - //create server - var app = express.createServer(); - - app.use(function (req, res, next) { - res.header("Server", serverName); - next(); - }); - - - //redirects browser to the pad's sanitized url if needed. otherwise, renders the html - app.param('pad', function (req, res, next, padId) { - //ensure the padname is valid and the url doesn't end with a / - if(!padManager.isValidPadId(padId) || /\/$/.test(req.url)) - { - res.send('Such a padname is forbidden', 404); - } - else - { - padManager.sanitizePadId(padId, function(sanitizedPadId) { - //the pad id was sanitized, so we redirect to the sanitized version - if(sanitizedPadId != padId) - { - var real_path = req.path.replace(/^\/p\/[^\/]+/, './' + sanitizedPadId); - res.header('Location', real_path); - res.send('You should be redirected to <a href="' + real_path + '">' + real_path + '</a>', 302); - } - //the pad id was fine, so just render it - else - { - next(); - } - }); - } - }); - - //load modules that needs a initalized db - readOnlyManager = require("./db/ReadOnlyManager"); - exporthtml = require("./utils/ExportHtml"); - exportHandler = require('./handler/ExportHandler'); - importHandler = require('./handler/ImportHandler'); - apiHandler = require('./handler/APIHandler'); - padManager = require('./db/PadManager'); - securityManager = require('./db/SecurityManager'); - socketIORouter = require("./handler/SocketIORouter"); - - //install logging - var httpLogger = log4js.getLogger("http"); - app.configure(function() - { - // Activate http basic auth if it has been defined in settings.json - if(settings.httpAuth != null) app.use(basic_auth); - - // If the log level specified in the config file is WARN or ERROR the application server never starts listening to requests as reported in issue #158. - // Not installing the log4js connect logger when the log level has a higher severity than INFO since it would not log at that level anyway. - if (!(settings.loglevel === "WARN" || settings.loglevel == "ERROR")) - app.use(log4js.connectLogger(httpLogger, { level: log4js.levels.INFO, format: ':status, :method :url'})); - app.use(express.cookieParser()); - }); - - app.error(function(err, req, res, next){ - res.send(500); - console.error(err.stack ? err.stack : err.toString()); - gracefulShutdown(); - }); - - // Cache both minified and static. - var assetCache = new CachingMiddleware; - app.all('/(minified|static)/*', assetCache.handle); - - // Minify will serve static files compressed (minify enabled). It also has - // file-specific hacks for ace/require-kernel/etc. - app.all('/static/:filename(*)', minify.minify); - - // Setup middleware that will package JavaScript files served by minify for - // CommonJS loader on the client-side. - var jsServer = new (Yajsml.Server)({ - rootPath: 'minified/' - , rootURI: 'http://localhost:' + settings.port + '/static/js/' - }); - var StaticAssociator = Yajsml.associators.StaticAssociator; - var associations = - Yajsml.associators.associationsForSimpleMapping(minify.tar); - var associator = new StaticAssociator(associations); - jsServer.setAssociator(associator); - app.use(jsServer); - - //checks for padAccess - function hasPadAccess(req, res, callback) - { - 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 - if(accessObj.accessStatus == "grant") - { - callback(); - } - //no access - else - { - res.send("403 - Can't touch this", 403); - } - }); - } - - //checks for basic http auth - function basic_auth (req, res, next) { - if (req.headers.authorization && req.headers.authorization.search('Basic ') === 0) { - // fetch login and password - if (new Buffer(req.headers.authorization.split(' ')[1], 'base64').toString() == settings.httpAuth) { - next(); - return; - } - } - - res.header('WWW-Authenticate', 'Basic realm="Protected Area"'); - if (req.headers.authorization) { - setTimeout(function () { - res.send('Authentication required', 401); - }, 1000); - } else { - res.send('Authentication required', 401); - } - } - - //serve read only pad - app.get('/ro/:id', function(req, res) - { - var html; - var padId; - var pad; - - async.series([ - //translate the read only pad to a padId - function(callback) - { - readOnlyManager.getPadId(req.params.id, function(err, _padId) - { - if(ERR(err, callback)) return; - - padId = _padId; - - //we need that to tell hasPadAcess about the pad - req.params.pad = padId; - - callback(); - }); - }, - //render the html document - function(callback) - { - //return if the there is no padId - if(padId == null) - { - callback("notfound"); - return; - } - - hasPadAccess(req, res, function() - { - //render the html document - exporthtml.getPadHTMLDocument(padId, null, false, function(err, _html) - { - if(ERR(err, callback)) return; - html = _html; - callback(); - }); - }); - } - ], function(err) - { - //throw any unexpected error - if(err && err != "notfound") - ERR(err); - - if(err == "notfound") - res.send('404 - Not Found', 404); - else - res.send(html); - }); - }); - - //serve pad.html under /p - app.get('/p/:pad', function(req, res, next) - { - var filePath = path.normalize(__dirname + "/../static/pad.html"); - res.sendfile(filePath, { maxAge: exports.maxAge }); - }); - - //serve timeslider.html under /p/$padname/timeslider - app.get('/p/:pad/timeslider', function(req, res, next) - { - var filePath = path.normalize(__dirname + "/../static/timeslider.html"); - res.sendfile(filePath, { maxAge: exports.maxAge }); - }); - - //serve timeslider.html under /p/$padname/timeslider - app.get('/p/:pad/:rev?/export/:type', function(req, res, next) - { - var types = ["pdf", "doc", "txt", "html", "odt", "dokuwiki"]; - //send a 404 if we don't support this filetype - if(types.indexOf(req.params.type) == -1) - { - next(); - return; - } - - //if abiword is disabled, and this is a format we only support with abiword, output a message - if(settings.abiword == null && - ["odt", "pdf", "doc"].indexOf(req.params.type) !== -1) - { - res.send("Abiword is not enabled at this Etherpad Lite instance. Set the path to Abiword in settings.json to enable this feature"); - return; - } - - res.header("Access-Control-Allow-Origin", "*"); - - hasPadAccess(req, res, function() - { - exportHandler.doExport(req, res, req.params.pad, req.params.type); - }); - }); - - //handle import requests - app.post('/p/:pad/import', function(req, res, next) - { - //if abiword is disabled, skip handling this request - if(settings.abiword == null) - { - next(); - return; - } - - hasPadAccess(req, res, function() - { - importHandler.doImport(req, res, req.params.pad); - }); - }); - - var apiLogger = log4js.getLogger("API"); - - //This is for making an api call, collecting all post information and passing it to the apiHandler - var apiCaller = function(req, res, fields) - { - res.header("Content-Type", "application/json; charset=utf-8"); - - apiLogger.info("REQUEST, " + req.params.func + ", " + JSON.stringify(fields)); - - //wrap the send function so we can log the response - res._send = res.send; - res.send = function(response) - { - response = JSON.stringify(response); - apiLogger.info("RESPONSE, " + req.params.func + ", " + response); - - //is this a jsonp call, if yes, add the function call - if(req.query.jsonp) - response = req.query.jsonp + "(" + response + ")"; - - res._send(response); - } - - //call the api handler - apiHandler.handle(req.params.func, fields, req, res); - } - - //This is a api GET call, collect all post informations and pass it to the apiHandler - app.get('/api/1/:func', function(req, res) - { - apiCaller(req, res, req.query) - }); - - //This is a api POST call, collect all post informations and pass it to the apiHandler - app.post('/api/1/:func', function(req, res) - { - new formidable.IncomingForm().parse(req, function(err, fields, files) - { - apiCaller(req, res, fields) - }); - }); - - //The Etherpad client side sends information about how a disconnect happen - app.post('/ep/pad/connection-diagnostic-info', function(req, res) - { - new formidable.IncomingForm().parse(req, function(err, fields, files) - { - console.log("DIAGNOSTIC-INFO: " + fields.diagnosticInfo); - res.end("OK"); - }); - }); - - //The Etherpad client side sends information about client side javscript errors - app.post('/jserror', function(req, res) - { - new formidable.IncomingForm().parse(req, function(err, fields, files) - { - console.error("CLIENT SIDE JAVASCRIPT ERROR: " + fields.errorInfo); - res.end("OK"); - }); - }); - - //serve index.html under / - app.get('/', function(req, res) - { - var filePath = path.normalize(__dirname + "/../static/index.html"); - res.sendfile(filePath, { maxAge: exports.maxAge }); - }); - - //serve robots.txt - app.get('/robots.txt', function(req, res) - { - var filePath = path.normalize(__dirname + "/../static/robots.txt"); - res.sendfile(filePath, { maxAge: exports.maxAge }); - }); - - //serve favicon.ico - app.get('/favicon.ico', function(req, res) - { - var filePath = path.normalize(__dirname + "/../static/custom/favicon.ico"); - res.sendfile(filePath, { maxAge: exports.maxAge }, function(err) - { - //there is no custom favicon, send the default favicon - if(err) - { - filePath = path.normalize(__dirname + "/../static/favicon.ico"); - res.sendfile(filePath, { maxAge: exports.maxAge }); - } - }); - }); - - //let the server listen - app.listen(settings.port, settings.ip); - console.log("Server is listening at " + settings.ip + ":" + settings.port); - - var onShutdown = false; - var gracefulShutdown = function(err) - { - if(err && err.stack) - { - console.error(err.stack); - } - else if(err) - { - console.error(err); - } - - //ensure there is only one graceful shutdown running - if(onShutdown) return; - onShutdown = true; - - console.log("graceful shutdown..."); - - //stop the http server - app.close(); - - //do the db shutdown - db.db.doShutdown(function() - { - console.log("db sucessfully closed."); - - process.exit(0); - }); - - setTimeout(function(){ - process.exit(1); - }, 3000); - } - - //connect graceful shutdown with sigint and uncaughtexception - if(os.type().indexOf("Windows") == -1) - { - //sigint is so far not working on windows - //https://github.com/joyent/node/issues/1553 - process.on('SIGINT', gracefulShutdown); - } - - process.on('uncaughtException', gracefulShutdown); - - //init socket.io and redirect all requests to the MessageHandler - var io = socketio.listen(app); - - //this is only a workaround to ensure it works with all browers behind a proxy - //we should remove this when the new socket.io version is more stable - io.set('transports', ['xhr-polling']); - - var socketIOLogger = log4js.getLogger("socket.io"); - io.set('logger', { - debug: function (str) - { - socketIOLogger.debug.apply(socketIOLogger, arguments); - }, - info: function (str) - { - socketIOLogger.info.apply(socketIOLogger, arguments); - }, - warn: function (str) - { - socketIOLogger.warn.apply(socketIOLogger, arguments); - }, - error: function (str) - { - socketIOLogger.error.apply(socketIOLogger, arguments); - }, - }); - - //minify socket.io javascript - if(settings.minify) - io.enable('browser client minification'); - - var padMessageHandler = require("./handler/PadMessageHandler"); - var timesliderMessageHandler = require("./handler/TimesliderMessageHandler"); - - //Initalize the Socket.IO Router - socketIORouter.setSocketIO(io); - socketIORouter.addComponent("pad", padMessageHandler); - socketIORouter.addComponent("timeslider", timesliderMessageHandler); - - callback(null); - } -]); diff --git a/settings.json.template b/settings.json.template index 94a60fd4..7aaa5d7e 100644 --- a/settings.json.template +++ b/settings.json.template @@ -13,7 +13,7 @@ "dbType" : "dirty", //the database specific settings "dbSettings" : { - "filename" : "../var/dirty.db" + "filename" : "var/dirty.db" }, /* An Example of MySQL Configuration @@ -39,9 +39,9 @@ but makes it impossible to debug the javascript/css */ "minify" : true, - /* How long may clients use served javascript code? Without versioning this - is may cause problems during deployment. */ - "maxAge" : 21600000, // 6 hours + /* How long may clients use served javascript code (in seconds)? Without versioning this + is may cause problems during deployment. Set to 0 to disable caching */ + "maxAge" : 21600, // 6 hours /* This is the path to the Abiword executable. Setting it to null, disables abiword. Abiword is needed to enable the import/export of pads*/ @@ -50,6 +50,12 @@ /* This setting is used if you need http basic auth */ // "httpAuth" : "user:pass", + /* This setting is used for http basic auth for admin pages. If not set, the admin page won't be accessible from web*/ + // "adminHttpAuth" : "user:pass", + /* The log level we are using, can be: DEBUG, INFO, WARN, ERROR */ - "loglevel": "INFO" + "loglevel": "INFO", + + /* cache 6 hours = 1000*60*60*6 */ + "maxAge": 21600000 } diff --git a/settings.json.template_windows b/settings.json.template_windows index 61f14dce..35b54d8d 100644 --- a/settings.json.template_windows +++ b/settings.json.template_windows @@ -12,7 +12,7 @@ "dbType" : "dirty", //the database specific settings "dbSettings" : { - "filename" : "../var/dirty.db" + "filename" : "var/dirty.db" }, /* An Example of MySQL Configuration @@ -40,5 +40,9 @@ /* This is the path to the Abiword executable. Setting it to null, disables abiword. Abiword is needed to enable the import/export of pads*/ - "abiword" : null + "abiword" : null, + + /* cache 6 hours = 1000*60*60*6 */ + "maxAge": 21600000 + } diff --git a/src/ep.json b/src/ep.json new file mode 100644 index 00000000..6bc77735 --- /dev/null +++ b/src/ep.json @@ -0,0 +1,16 @@ +{ + "parts": [ + { "name": "static", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/static: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" } }, + { "name": "webaccess", "hooks": { "expressConfigure": "ep_etherpad-lite/node/hooks/express/webaccess:expressConfigure" } }, + { "name": "apicalls", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/apicalls:expressCreateServer" } }, + { "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": "adminplugins", "hooks": { + "expressCreateServer": "ep_etherpad-lite/node/hooks/express/adminplugins:expressCreateServer", + "socketio": "ep_etherpad-lite/node/hooks/express/adminplugins:socketio" } } + ] +} diff --git a/node/README.md b/src/node/README.md index 4b443289..4b443289 100644 --- a/node/README.md +++ b/src/node/README.md diff --git a/node/db/API.js b/src/node/db/API.js index 09cc95af..09cc95af 100644 --- a/node/db/API.js +++ b/src/node/db/API.js diff --git a/node/db/AuthorManager.js b/src/node/db/AuthorManager.js index 9baf6347..f644de12 100644 --- a/node/db/AuthorManager.js +++ b/src/node/db/AuthorManager.js @@ -18,11 +18,11 @@ * limitations under the License. */ -var CommonCode = require('../utils/common_code'); + var ERR = require("async-stacktrace"); var db = require("./DB").db; var async = require("async"); -var randomString = CommonCode.require('/pad_utils').randomString; +var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; /** * Checks if the author exists diff --git a/node/db/DB.js b/src/node/db/DB.js index 7273c83e..7273c83e 100644 --- a/node/db/DB.js +++ b/src/node/db/DB.js diff --git a/node/db/GroupManager.js b/src/node/db/GroupManager.js index 04c79cfa..bd19507f 100644 --- a/node/db/GroupManager.js +++ b/src/node/db/GroupManager.js @@ -18,10 +18,10 @@ * limitations under the License. */ -var CommonCode = require('../utils/common_code'); + var ERR = require("async-stacktrace"); var customError = require("../utils/customError"); -var randomString = CommonCode.require('/pad_utils').randomString; +var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; var db = require("./DB").db; var async = require("async"); var padManager = require("./PadManager"); diff --git a/node/db/Pad.js b/src/node/db/Pad.js index 40875eff..b4a39c17 100644 --- a/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -2,11 +2,11 @@ * The pad object, defined with joose */ -var CommonCode = require('../utils/common_code'); + var ERR = require("async-stacktrace"); -var Changeset = CommonCode.require("/Changeset"); -var AttributePoolFactory = CommonCode.require("/AttributePoolFactory"); -var randomString = CommonCode.require('/pad_utils').randomString; +var Changeset = require("ep_etherpad-lite/static/js/Changeset"); +var AttributePool = require("ep_etherpad-lite/static/js/AttributePool"); +var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; var db = require("./DB").db; var async = require("async"); var settings = require('../utils/Settings'); @@ -15,6 +15,11 @@ var padManager = require("./PadManager"); var padMessageHandler = require("../handler/PadMessageHandler"); var readOnlyManager = require("./ReadOnlyManager"); var crypto = require("crypto"); +var randomString = require("../utils/randomstring"); + +//serialization/deserialization attributes +var attributeBlackList = ["id"]; +var jsonableList = ["pool"]; /** * Copied from the Etherpad source code. It converts Windows line breaks to Unix line breaks and convert Tabs to spaces @@ -28,13 +33,13 @@ exports.cleanText = function (txt) { var Pad = function Pad(id) { this.atext = Changeset.makeAText("\n"); - this.pool = AttributePoolFactory.createAttributePool(); + this.pool = new AttributePool(); this.head = -1; this.chatHead = -1; this.publicStatus = false; this.passwordHash = null; this.id = id; - + this.savedRevisions = []; }; exports.Pad = Pad; @@ -75,15 +80,28 @@ Pad.prototype.appendRevision = function appendRevision(aChangeset, author) { newRevData.meta.atext = this.atext; } - db.set("pad:"+this.id+":revs:"+newRev, newRevData); - db.set("pad:"+this.id, {atext: this.atext, - pool: this.pool.toJsonable(), - head: this.head, - chatHead: this.chatHead, - publicStatus: this.publicStatus, - passwordHash: this.passwordHash}); + db.set("pad:"+this.id+":revs:"+newRev, newRevData); + this.saveToDatabase(); }; +//save all attributes to the database +Pad.prototype.saveToDatabase = function saveToDatabase(){ + var dbObject = {}; + + for(var attr in this){ + if(typeof this[attr] === "function") continue; + if(attributeBlackList.indexOf(attr) !== -1) continue; + + dbObject[attr] = this[attr]; + + if(jsonableList.indexOf(attr) !== -1){ + dbObject[attr] = dbObject[attr].toJsonable(); + } + } + + db.set("pad:"+this.id, dbObject); +} + Pad.prototype.getRevisionChangeset = function getRevisionChangeset(revNum, callback) { db.getSub("pad:"+this.id+":revs:"+revNum, ["changeset"], callback); }; @@ -200,11 +218,10 @@ Pad.prototype.setText = function setText(newText) { }; Pad.prototype.appendChatMessage = function appendChatMessage(text, userId, time) { - this.chatHead++; - //save the chat entry in the database - db.set("pad:"+this.id+":chat:"+this.chatHead, {"text": text, "userId": userId, "time": time}); - //save the new chat head - db.setSub("pad:"+this.id, ["chatHead"], this.chatHead); + this.chatHead++; + //save the chat entry in the database + db.set("pad:"+this.id+":chat:"+this.chatHead, {"text": text, "userId": userId, "time": time}); + this.saveToDatabase(); }; Pad.prototype.getChatMessage = function getChatMessage(entryNum, callback) { @@ -324,27 +341,14 @@ Pad.prototype.init = function init(text, callback) { //if this pad exists, load it if(value != null) { - _this.head = value.head; - _this.atext = value.atext; - _this.pool = _this.pool.fromJsonable(value.pool); - - //ensure we have a local chatHead variable - if(value.chatHead != null) - _this.chatHead = value.chatHead; - else - _this.chatHead = -1; - - //ensure we have a local publicStatus variable - if(value.publicStatus != null) - _this.publicStatus = value.publicStatus; - else - _this.publicStatus = false; - - //ensure we have a local passwordHash variable - if(value.passwordHash != null) - _this.passwordHash = value.passwordHash; - else - _this.passwordHash = null; + //copy all attr. To a transfrom via fromJsonable if necassary + for(var attr in value){ + if(jsonableList.indexOf(attr) !== -1){ + _this[attr] = _this[attr].fromJsonable(value[attr]); + } else { + _this[attr] = value[attr]; + } + } } //this pad doesn't exist, so create it else @@ -452,12 +456,12 @@ Pad.prototype.remove = function remove(callback) { //set in db Pad.prototype.setPublicStatus = function setPublicStatus(publicStatus) { this.publicStatus = publicStatus; - db.setSub("pad:"+this.id, ["publicStatus"], this.publicStatus); + this.saveToDatabase(); }; Pad.prototype.setPassword = function setPassword(password) { this.passwordHash = password == null ? null : hash(password, generateSalt()); - db.setSub("pad:"+this.id, ["passwordHash"], this.passwordHash); + this.saveToDatabase(); }; Pad.prototype.isCorrectPassword = function isCorrectPassword(password) { @@ -468,6 +472,31 @@ Pad.prototype.isPasswordProtected = function isPasswordProtected() { return this.passwordHash != null; }; +Pad.prototype.addSavedRevision = function addSavedRevision(revNum, savedById, label) { + //if this revision is already saved, return silently + for(var i in this.savedRevisions){ + if(this.savedRevisions.revNum === revNum){ + return; + } + } + + //build the saved revision object + var savedRevision = {}; + savedRevision.revNum = revNum; + savedRevision.savedById = savedById; + savedRevision.label = label || "Revision " + revNum; + savedRevision.timestamp = new Date().getTime(); + savedRevision.id = randomString(10); + + //save this new saved revision + this.savedRevisions.push(savedRevision); + this.saveToDatabase(); +}; + +Pad.prototype.getSavedRevisions = function getSavedRevisions() { + return this.savedRevisions; +}; + /* Crypto helper methods */ function hash(password, salt) diff --git a/node/db/PadManager.js b/src/node/db/PadManager.js index 5f08b1b1..5f08b1b1 100644 --- a/node/db/PadManager.js +++ b/src/node/db/PadManager.js diff --git a/node/db/ReadOnlyManager.js b/src/node/db/ReadOnlyManager.js index e5dab99b..34340630 100644 --- a/node/db/ReadOnlyManager.js +++ b/src/node/db/ReadOnlyManager.js @@ -18,11 +18,11 @@ * limitations under the License. */ -var CommonCode = require('../utils/common_code'); + var ERR = require("async-stacktrace"); var db = require("./DB").db; var async = require("async"); -var randomString = CommonCode.require('/pad_utils').randomString; +var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; /** * returns a read only id for a pad diff --git a/node/db/SecurityManager.js b/src/node/db/SecurityManager.js index 33ab37d4..a092453a 100644 --- a/node/db/SecurityManager.js +++ b/src/node/db/SecurityManager.js @@ -18,7 +18,7 @@ * limitations under the License. */ -var CommonCode = require('../utils/common_code'); + var ERR = require("async-stacktrace"); var db = require("./DB").db; var async = require("async"); @@ -26,7 +26,7 @@ var authorManager = require("./AuthorManager"); var padManager = require("./PadManager"); var sessionManager = require("./SessionManager"); var settings = require("../utils/Settings") -var randomString = CommonCode.require('/pad_utils').randomString; +var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; /** * This function controlls the access to a pad, it checks if the user can access a pad. diff --git a/node/db/SessionManager.js b/src/node/db/SessionManager.js index c5af33c6..ec4948a6 100644 --- a/node/db/SessionManager.js +++ b/src/node/db/SessionManager.js @@ -18,10 +18,10 @@ * limitations under the License. */ -var CommonCode = require('../utils/common_code'); + var ERR = require("async-stacktrace"); var customError = require("../utils/customError"); -var randomString = CommonCode.require('/pad_utils').randomString; +var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; var db = require("./DB").db; var async = require("async"); var groupMangager = require("./GroupManager"); diff --git a/node/easysync_tests.js b/src/node/easysync_tests.js index 8e7398be..374e949f 100644 --- a/node/easysync_tests.js +++ b/src/node/easysync_tests.js @@ -20,9 +20,9 @@ * limitations under the License. */ -var CommonCode = require('./utils/common_code'); -var Changeset = CommonCode.require("/Changeset"); -var AttributePoolFactory = CommonCode.require("/AttributePoolFactory"); + +var Changeset = require("ep_etherpad-lite/static/js/Changeset"); +var AttributePool = require("ep_etherpad-lite/static/js/AttributePool"); function random() { this.nextInt = function (maxValue) { @@ -227,7 +227,7 @@ function runTests() { return attribs; // it's already an attrib pool } else { // assume it's an array of attrib strings to be split and added - var p = AttributePoolFactory.createAttributePool(); + var p = new AttributePool(); attribs.forEach(function (kv) { p.putAttrib(kv.split(',')); }); @@ -325,7 +325,7 @@ function runTests() { runMutateAttributionTest(4, ['foo,bar', 'line,1', 'line,2', 'line,3', 'line,4', 'line,5'], "Z:5>1|2=2+1$x", ["?*1|1+1", "?*2|1+1", "*3|1+1", "?*4|1+1", "?*5|1+1"], ["?*1|1+1", "?*2|1+1", "+1*3|1+1", "?*4|1+1", "?*5|1+1"]); var testPoolWithChars = (function () { - var p = AttributePoolFactory.createAttributePool(); + var p = new AttributePool(); p.putAttrib(['char', 'newline']); for (var i = 1; i < 36; i++) { p.putAttrib(['char', Changeset.numToString(i)]); @@ -560,7 +560,7 @@ function runTests() { var rand = new random(); print("> testCompose#" + randomSeed); - var p = AttributePoolFactory.createAttributePool(); + var p = new AttributePool(); var startText = randomMultiline(10, 20, rand) + '\n'; @@ -594,7 +594,7 @@ function runTests() { (function simpleComposeAttributesTest() { print("> simpleComposeAttributesTest"); - var p = AttributePoolFactory.createAttributePool(); + var p = new AttributePool(); p.putAttrib(['bold', '']); p.putAttrib(['bold', 'true']); var cs1 = Changeset.checkRep("Z:2>1*1+1*1=1$x"); @@ -604,7 +604,7 @@ function runTests() { })(); (function followAttributesTest() { - var p = AttributePoolFactory.createAttributePool(); + var p = new AttributePool(); p.putAttrib(['x', '']); p.putAttrib(['x', 'abc']); p.putAttrib(['x', 'def']); @@ -633,7 +633,7 @@ function runTests() { var rand = new random(); print("> testFollow#" + randomSeed); - var p = AttributePoolFactory.createAttributePool(); + var p = new AttributePool(); var startText = randomMultiline(10, 20, rand) + '\n'; @@ -682,8 +682,8 @@ function runTests() { (function testMoveOpsToNewPool() { print("> testMoveOpsToNewPool"); - var pool1 = AttributePoolFactory.createAttributePool(); - var pool2 = AttributePoolFactory.createAttributePool(); + var pool1 = new AttributePool(); + var pool2 = new AttributePool(); pool1.putAttrib(['baz', 'qux']); pool1.putAttrib(['foo', 'bar']); @@ -738,7 +738,7 @@ function runTests() { (function testOpAttributeValue() { print("> testOpAttributeValue"); - var p = AttributePoolFactory.createAttributePool(); + var p = new AttributePool(); p.putAttrib(['name', 'david']); p.putAttrib(['color', 'green']); diff --git a/src/node/eejs/examples/bar.ejs b/src/node/eejs/examples/bar.ejs new file mode 100644 index 00000000..6a2cc4ba --- /dev/null +++ b/src/node/eejs/examples/bar.ejs @@ -0,0 +1,9 @@ +a +<% e.begin_block("bar"); %> + A + <% e.begin_block("foo"); %> + XX + <% e.end_block(); %> + B +<% e.end_block(); %> +b diff --git a/src/node/eejs/examples/foo.ejs b/src/node/eejs/examples/foo.ejs new file mode 100644 index 00000000..daee5f8e --- /dev/null +++ b/src/node/eejs/examples/foo.ejs @@ -0,0 +1,7 @@ +<% e.inherit("./bar.ejs"); %> + +<% e.begin_define_block("foo"); %> + YY + <% e.super(); %> + ZZ +<% e.end_define_block(); %> diff --git a/src/node/eejs/index.js b/src/node/eejs/index.js new file mode 100644 index 00000000..90c69e59 --- /dev/null +++ b/src/node/eejs/index.js @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2011 RedHog (Egil Möller) <egil.moller@freecode.no> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* Basic usage: + * + * require("./index").require("./examples/foo.ejs") + */ + +var ejs = require("ejs"); +var fs = require("fs"); +var path = require("path"); +var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js"); + +exports.info = { + buf_stack: [], + block_stack: [], + blocks: {}, + file_stack: [], +}; + +exports._init = function (b, recursive) { + exports.info.buf_stack.push(exports.info.buf); + exports.info.buf = b; +} + +exports._exit = function (b, recursive) { + exports.info.file_stack[exports.info.file_stack.length-1].inherit.forEach(function (item) { + exports._require(item.name, item.args); + }); + exports.info.buf = exports.info.buf_stack.pop(); +} + +exports.begin_capture = function() { + exports.info.buf_stack.push(exports.info.buf.concat()); + exports.info.buf.splice(0, exports.info.buf.length); +} + +exports.end_capture = function () { + var res = exports.info.buf.join(""); + exports.info.buf.splice.apply( + exports.info.buf, + [0, exports.info.buf.length].concat(exports.info.buf_stack.pop())); + return res; +} + +exports.begin_define_block = function (name) { + if (typeof exports.info.blocks[name] == "undefined") + exports.info.blocks[name] = {}; + exports.info.block_stack.push(name); + exports.begin_capture(); +} + +exports.super = function () { + exports.info.buf.push('<!eejs!super!>'); +} + +exports.end_define_block = function () { + content = exports.end_capture(); + var name = exports.info.block_stack.pop(); + if (typeof exports.info.blocks[name].content == "undefined") + exports.info.blocks[name].content = content; + else if (typeof exports.info.blocks[name].content.indexOf('<!eejs!super!>')) + exports.info.blocks[name].content = exports.info.blocks[name].content.replace('<!eejs!super!>', content); + + return exports.info.blocks[name].content; +} + +exports.end_block = function () { + var name = exports.info.block_stack[exports.info.block_stack.length-1]; + var args = {content: exports.end_define_block()}; + hooks.callAll("eejsBlock_" + name, args); + exports.info.buf.push(args.content); +} + +exports.begin_block = exports.begin_define_block; + +exports.inherit = function (name, args) { + exports.info.file_stack[exports.info.file_stack.length-1].inherit.push({name:name, args:args}); +} + +exports.require = function (name, args) { + if (args == undefined) args = {}; + + if ((name.indexOf("./") == 0 || name.indexOf("../") == 0) && exports.info.file_stack.length) { + name = path.join(path.dirname(exports.info.file_stack[exports.info.file_stack.length-1].path), name); + } + var ejspath = require.resolve(name) + + args.e = exports; + args.require = require; + var template = '<% e._init(buf); %>' + fs.readFileSync(ejspath).toString() + '<% e._exit(); %>'; + + exports.info.file_stack.push({path: ejspath, inherit: []}); + var res = ejs.render(template, args); + exports.info.file_stack.pop(); + + return res; +} + +exports._require = function (name, args) { + exports.info.buf.push(exports.require(name, args)); +} diff --git a/node/handler/APIHandler.js b/src/node/handler/APIHandler.js index a7f66151..d4d7d6ce 100644 --- a/node/handler/APIHandler.js +++ b/src/node/handler/APIHandler.js @@ -18,12 +18,12 @@ * limitations under the License. */ -var CommonCode = require('../utils/common_code'); + var ERR = require("async-stacktrace"); var fs = require("fs"); var api = require("../db/API"); var padManager = require("../db/PadManager"); -var randomString = CommonCode.require('/pad_utils').randomString; +var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; //ensure we have an apikey var apikey = null; diff --git a/node/handler/ExportHandler.js b/src/node/handler/ExportHandler.js index 1b7fcc26..1b7fcc26 100644 --- a/node/handler/ExportHandler.js +++ b/src/node/handler/ExportHandler.js diff --git a/node/handler/ImportHandler.js b/src/node/handler/ImportHandler.js index ed5eb05e..ed5eb05e 100644 --- a/node/handler/ImportHandler.js +++ b/src/node/handler/ImportHandler.js diff --git a/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 48f0aa98..4a570e21 100644 --- a/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -18,18 +18,21 @@ * limitations under the License. */ -var CommonCode = require('../utils/common_code'); + var ERR = require("async-stacktrace"); var async = require("async"); var padManager = require("../db/PadManager"); -var Changeset = CommonCode.require("/Changeset"); -var AttributePoolFactory = CommonCode.require("/AttributePoolFactory"); +var Changeset = require("ep_etherpad-lite/static/js/Changeset"); +var AttributePool = require("ep_etherpad-lite/static/js/AttributePool"); +var AttributeManager = require("ep_etherpad-lite/static/js/AttributeManager"); var authorManager = require("../db/AuthorManager"); var readOnlyManager = require("../db/ReadOnlyManager"); var settings = require('../utils/Settings'); var securityManager = require("../db/SecurityManager"); +var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins.js"); var log4js = require('log4js'); var messageLogger = log4js.getLogger("message"); +var _ = require('underscore'); /** * A associative array that translates a session to a pad @@ -127,7 +130,11 @@ exports.handleDisconnect = function(client) //Go trough all user that are still on the pad, and send them the USER_LEAVE message for(i in pad2sessions[sessionPad]) { - socketio.sockets.sockets[pad2sessions[sessionPad][i]].json.send(messageToTheOtherUsers); + var socket = socketio.sockets.sockets[pad2sessions[sessionPad][i]]; + if(socket !== undefined){ + socket.json.send(messageToTheOtherUsers); + } + } }); } @@ -186,6 +193,11 @@ exports.handleMessage = function(client, message) handleChatMessage(client, message); } else if(message.type == "COLLABROOM" && + message.data.type == "SAVE_REVISION") + { + handleSaveRevisionMessage(client, message); + } + else if(message.type == "COLLABROOM" && message.data.type == "CLIENT_MESSAGE" && message.data.payload.type == "suggestUserName") { @@ -199,6 +211,23 @@ exports.handleMessage = function(client, message) } /** + * Handles a save revision message + * @param client the client that send this message + * @param message the message from the client + */ +function handleSaveRevisionMessage(client, message){ + var padId = session2pad[client.id]; + var userId = sessioninfos[client.id].author; + + padManager.getPad(padId, function(err, pad) + { + if(ERR(err)) return; + + pad.addSavedRevision(pad.head, userId); + }); +} + +/** * Handles a Chat Message * @param client the client that send this message * @param message the message from the client @@ -367,7 +396,7 @@ function handleUserChanges(client, message) //get all Vars we need var baseRev = message.data.baseRev; - var wireApool = (AttributePoolFactory.createAttributePool()).fromJsonable(message.data.apool); + var wireApool = (new AttributePool()).fromJsonable(message.data.apool); var changeset = message.data.changeset; var r, apool, pad; @@ -564,8 +593,12 @@ function _correctMarkersInPad(atext, apool) { var offset = 0; while (iter.hasNext()) { var op = iter.next(); - var listValue = Changeset.opAttributeValue(op, 'list', apool); - if (listValue) { + + var hasMarker = _.find(AttributeManager.lineAttributes, function(attribute){ + return Changeset.opAttributeValue(op, attribute, apool); + }) !== undefined; + + if (hasMarker) { for(var i=0;i<op.chars;i++) { if (offset > 0 && text.charAt(offset-1) != '\n') { badMarkers.push(offset); @@ -737,9 +770,10 @@ function handleClientReady(client, message) { for(var i in pad2sessions[message.padId]) { - if(sessioninfos[pad2sessions[message.padId][i]].author == author) + if(sessioninfos[pad2sessions[message.padId][i]] && sessioninfos[pad2sessions[message.padId][i]].author == author) { - socketio.sockets.sockets[pad2sessions[message.padId][i]].json.send({disconnect:"userdup"}); + var socket = socketio.sockets.sockets[pad2sessions[message.padId][i]]; + if(socket) socket.json.send({disconnect:"userdup"}); } } } @@ -800,9 +834,12 @@ function handleClientReady(client, message) "hideSidebar": false }, "abiwordAvailable": settings.abiwordAvailable(), - "hooks": {} + "plugins": { + "plugins": plugins.plugins, + "parts": plugins.parts, + } } - + //Add a username to the clientVars if one avaiable if(authorName != null) { diff --git a/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.js index f3b82b8c..f3b82b8c 100644 --- a/node/handler/SocketIORouter.js +++ b/src/node/handler/SocketIORouter.js diff --git a/node/handler/TimesliderMessageHandler.js b/src/node/handler/TimesliderMessageHandler.js index 18806843..a6cf8f4d 100644 --- a/node/handler/TimesliderMessageHandler.js +++ b/src/node/handler/TimesliderMessageHandler.js @@ -18,12 +18,12 @@ * limitations under the License. */ -var CommonCode = require('../utils/common_code'); + var ERR = require("async-stacktrace"); var async = require("async"); var padManager = require("../db/PadManager"); -var Changeset = CommonCode.require("/Changeset"); -var AttributePoolFactory = CommonCode.require("/AttributePoolFactory"); +var Changeset = require("ep_etherpad-lite/static/js/Changeset"); +var AttributePool = require("ep_etherpad-lite/static/js/AttributePool"); var settings = require('../utils/Settings'); var authorManager = require("../db/AuthorManager"); var log4js = require('log4js'); @@ -166,6 +166,7 @@ function createTimesliderClientVars (padId, callback) hooks: [], initialStyledContents: {} }; + var pad; var initialChangesets = []; @@ -180,6 +181,12 @@ function createTimesliderClientVars (padId, callback) callback(); }); }, + //get all saved revisions and add them + function(callback) + { + clientVars.savedRevisions = pad.getSavedRevisions(); + callback(); + }, //get all authors and add them to function(callback) { @@ -265,7 +272,7 @@ function getChangesetInfo(padId, startNum, endNum, granularity, callback) var forwardsChangesets = []; var backwardsChangesets = []; var timeDeltas = []; - var apool = AttributePoolFactory.createAttributePool(); + var apool = new AttributePool(); var pad; var composedChangesets = {}; var revisionDate = []; diff --git a/src/node/hooks/express/adminplugins.js b/src/node/hooks/express/adminplugins.js new file mode 100644 index 00000000..fa7e7077 --- /dev/null +++ b/src/node/hooks/express/adminplugins.js @@ -0,0 +1,51 @@ +var path = require('path'); +var eejs = require('ep_etherpad-lite/node/eejs'); +var installer = require('ep_etherpad-lite/static/js/pluginfw/installer'); +var plugins = require('ep_etherpad-lite/static/js/pluginfw/plugins'); + +exports.expressCreateServer = function (hook_name, args, cb) { + args.app.get('/admin/plugins', function(req, res) { + var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins"); + var render_args = { + plugins: plugins.plugins, + search_results: {}, + errors: [], + }; + + res.send(eejs.require( + "ep_etherpad-lite/templates/admin/plugins.html", + render_args), {}); + }); +} + +exports.socketio = function (hook_name, args, cb) { + var io = args.io.of("/pluginfw/installer"); + io.on('connection', function (socket) { + socket.on("load", function (query) { + socket.emit("installed-results", {results: plugins.plugins}); + }); + + socket.on("search", function (query) { + socket.emit("progress", {progress:0, message:'Fetching results...'}); + installer.search(query, function (progress) { + if (progress.results) + socket.emit("search-result", progress); + socket.emit("progress", progress); + }); + }); + + socket.on("install", function (plugin_name) { + socket.emit("progress", {progress:0, message:'Downloading and installing ' + plugin_name + "..."}); + installer.install(plugin_name, function (progress) { + socket.emit("progress", progress); + }); + }); + + socket.on("uninstall", function (plugin_name) { + socket.emit("progress", {progress:0, message:'Uninstalling ' + plugin_name + "..."}); + installer.uninstall(plugin_name, function (progress) { + socket.emit("progress", progress); + }); + }); + }); +} diff --git a/src/node/hooks/express/apicalls.js b/src/node/hooks/express/apicalls.js new file mode 100644 index 00000000..48d50722 --- /dev/null +++ b/src/node/hooks/express/apicalls.js @@ -0,0 +1,60 @@ +var log4js = require('log4js'); +var apiLogger = log4js.getLogger("API"); +var formidable = require('formidable'); +var apiHandler = require('../../handler/APIHandler'); + +//This is for making an api call, collecting all post information and passing it to the apiHandler +var apiCaller = function(req, res, fields) { + res.header("Content-Type", "application/json; charset=utf-8"); + + apiLogger.info("REQUEST, " + req.params.func + ", " + JSON.stringify(fields)); + + //wrap the send function so we can log the response + //note: res._send seems to be already in use, so better use a "unique" name + res._____send = res.send; + res.send = function (response) { + response = JSON.stringify(response); + apiLogger.info("RESPONSE, " + req.params.func + ", " + response); + + //is this a jsonp call, if yes, add the function call + if(req.query.jsonp) + response = req.query.jsonp + "(" + response + ")"; + + res._____send(response); + } + + //call the api handler + apiHandler.handle(req.params.func, fields, req, res); +} + +exports.apiCaller = apiCaller; + +exports.expressCreateServer = function (hook_name, args, cb) { + //This is a api GET call, collect all post informations and pass it to the apiHandler + args.app.get('/api/1/:func', function (req, res) { + apiCaller(req, res, req.query) + }); + + //This is a api POST call, collect all post informations and pass it to the apiHandler + args.app.post('/api/1/:func', function(req, res) { + new formidable.IncomingForm().parse(req, function (err, fields, files) { + apiCaller(req, res, fields) + }); + }); + + //The Etherpad client side sends information about how a disconnect happen + args.app.post('/ep/pad/connection-diagnostic-info', function(req, res) { + new formidable.IncomingForm().parse(req, function(err, fields, files) { + console.log("DIAGNOSTIC-INFO: " + fields.diagnosticInfo); + res.end("OK"); + }); + }); + + //The Etherpad client side sends information about client side javscript errors + args.app.post('/jserror', function(req, res) { + new formidable.IncomingForm().parse(req, function(err, fields, files) { + console.error("CLIENT SIDE JAVASCRIPT ERROR: " + fields.errorInfo); + res.end("OK"); + }); + }); +} diff --git a/src/node/hooks/express/errorhandling.js b/src/node/hooks/express/errorhandling.js new file mode 100644 index 00000000..cb8c5898 --- /dev/null +++ b/src/node/hooks/express/errorhandling.js @@ -0,0 +1,52 @@ +var os = require("os"); +var db = require('../../db/DB'); + + +exports.onShutdown = false; +exports.gracefulShutdown = function(err) { + if(err && err.stack) { + console.error(err.stack); + } else if(err) { + console.error(err); + } + + //ensure there is only one graceful shutdown running + if(exports.onShutdown) return; + exports.onShutdown = true; + + console.log("graceful shutdown..."); + + //stop the http server + exports.app.close(); + + //do the db shutdown + db.db.doShutdown(function() { + console.log("db sucessfully closed."); + + process.exit(0); + }); + + setTimeout(function(){ + process.exit(1); + }, 3000); +} + + +exports.expressCreateServer = function (hook_name, args, cb) { + exports.app = args.app; + + args.app.error(function(err, req, res, next){ + res.send(500); + console.error(err.stack ? err.stack : err.toString()); + exports.gracefulShutdown(); + }); + + //connect graceful shutdown with sigint and uncaughtexception + if(os.type().indexOf("Windows") == -1) { + //sigint is so far not working on windows + //https://github.com/joyent/node/issues/1553 + process.on('SIGINT', exports.gracefulShutdown); + } + + process.on('uncaughtException', exports.gracefulShutdown); +} diff --git a/src/node/hooks/express/importexport.js b/src/node/hooks/express/importexport.js new file mode 100644 index 00000000..9e78f34d --- /dev/null +++ b/src/node/hooks/express/importexport.js @@ -0,0 +1,41 @@ +var hasPadAccess = require("../../padaccess"); +var settings = require('../../utils/Settings'); +var exportHandler = require('../../handler/ExportHandler'); +var importHandler = require('../../handler/ImportHandler'); + +exports.expressCreateServer = function (hook_name, args, cb) { + args.app.get('/p/:pad/:rev?/export/:type', function(req, res, next) { + var types = ["pdf", "doc", "txt", "html", "odt", "dokuwiki"]; + //send a 404 if we don't support this filetype + if (types.indexOf(req.params.type) == -1) { + next(); + return; + } + + //if abiword is disabled, and this is a format we only support with abiword, output a message + if (settings.abiword == null && + ["odt", "pdf", "doc"].indexOf(req.params.type) !== -1) { + res.send("Abiword is not enabled at this Etherpad Lite instance. Set the path to Abiword in settings.json to enable this feature"); + return; + } + + res.header("Access-Control-Allow-Origin", "*"); + + hasPadAccess(req, res, function() { + exportHandler.doExport(req, res, req.params.pad, req.params.type); + }); + }); + + //handle import requests + args.app.post('/p/:pad/import', function(req, res, next) { + //if abiword is disabled, skip handling this request + if(settings.abiword == null) { + next(); + return; + } + + hasPadAccess(req, res, function() { + importHandler.doImport(req, res, req.params.pad); + }); + }); +} diff --git a/src/node/hooks/express/padreadonly.js b/src/node/hooks/express/padreadonly.js new file mode 100644 index 00000000..60ece0ad --- /dev/null +++ b/src/node/hooks/express/padreadonly.js @@ -0,0 +1,65 @@ +var async = require('async'); +var ERR = require("async-stacktrace"); +var readOnlyManager = require("../../db/ReadOnlyManager"); +var hasPadAccess = require("../../padaccess"); +var exporthtml = require("../../utils/ExportHtml"); + +exports.expressCreateServer = function (hook_name, args, cb) { + //serve read only pad + args.app.get('/ro/:id', function(req, res) + { + var html; + var padId; + var pad; + + async.series([ + //translate the read only pad to a padId + function(callback) + { + readOnlyManager.getPadId(req.params.id, function(err, _padId) + { + if(ERR(err, callback)) return; + + padId = _padId; + + //we need that to tell hasPadAcess about the pad + req.params.pad = padId; + + callback(); + }); + }, + //render the html document + function(callback) + { + //return if the there is no padId + if(padId == null) + { + callback("notfound"); + return; + } + + hasPadAccess(req, res, function() + { + //render the html document + exporthtml.getPadHTMLDocument(padId, null, false, function(err, _html) + { + if(ERR(err, callback)) return; + html = _html; + callback(); + }); + }); + } + ], function(err) + { + //throw any unexpected error + if(err && err != "notfound") + ERR(err); + + if(err == "notfound") + res.send('404 - Not Found', 404); + else + res.send(html); + }); + }); + +}
\ No newline at end of file diff --git a/src/node/hooks/express/padurlsanitize.js b/src/node/hooks/express/padurlsanitize.js new file mode 100644 index 00000000..4f5dd7a5 --- /dev/null +++ b/src/node/hooks/express/padurlsanitize.js @@ -0,0 +1,29 @@ +var padManager = require('../../db/PadManager'); + +exports.expressCreateServer = function (hook_name, args, cb) { + //redirects browser to the pad's sanitized url if needed. otherwise, renders the html + args.app.param('pad', function (req, res, next, padId) { + //ensure the padname is valid and the url doesn't end with a / + if(!padManager.isValidPadId(padId) || /\/$/.test(req.url)) + { + res.send('Such a padname is forbidden', 404); + } + else + { + padManager.sanitizePadId(padId, function(sanitizedPadId) { + //the pad id was sanitized, so we redirect to the sanitized version + if(sanitizedPadId != padId) + { + var real_path = req.path.replace(/^\/p\/[^\/]+/, '/p/' + sanitizedPadId); + res.header('Location', real_path); + res.send('You should be redirected to <a href="' + real_path + '">' + real_path + '</a>', 302); + } + //the pad id was fine, so just render it + else + { + next(); + } + }); + } + }); +} diff --git a/src/node/hooks/express/socketio.js b/src/node/hooks/express/socketio.js new file mode 100644 index 00000000..e040f7ac --- /dev/null +++ b/src/node/hooks/express/socketio.js @@ -0,0 +1,49 @@ +var log4js = require('log4js'); +var socketio = require('socket.io'); +var settings = require('../../utils/Settings'); +var socketIORouter = require("../../handler/SocketIORouter"); +var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); + +var padMessageHandler = require("../../handler/PadMessageHandler"); +var timesliderMessageHandler = require("../../handler/TimesliderMessageHandler"); + + +exports.expressCreateServer = function (hook_name, args, cb) { + //init socket.io and redirect all requests to the MessageHandler + var io = socketio.listen(args.app); + + //this is only a workaround to ensure it works with all browers behind a proxy + //we should remove this when the new socket.io version is more stable + io.set('transports', ['xhr-polling']); + + var socketIOLogger = log4js.getLogger("socket.io"); + io.set('logger', { + debug: function (str) + { + socketIOLogger.debug.apply(socketIOLogger, arguments); + }, + info: function (str) + { + socketIOLogger.info.apply(socketIOLogger, arguments); + }, + warn: function (str) + { + socketIOLogger.warn.apply(socketIOLogger, arguments); + }, + error: function (str) + { + socketIOLogger.error.apply(socketIOLogger, arguments); + }, + }); + + //minify socket.io javascript + if(settings.minify) + io.enable('browser client minification'); + + //Initalize the Socket.IO Router + socketIORouter.setSocketIO(io); + socketIORouter.addComponent("pad", padMessageHandler); + socketIORouter.addComponent("timeslider", timesliderMessageHandler); + + hooks.callAll("socketio", {"app": args.app, "io": io}); +} diff --git a/src/node/hooks/express/specialpages.js b/src/node/hooks/express/specialpages.js new file mode 100644 index 00000000..474f475e --- /dev/null +++ b/src/node/hooks/express/specialpages.js @@ -0,0 +1,46 @@ +var path = require('path'); +var eejs = require('ep_etherpad-lite/node/eejs'); + +exports.expressCreateServer = function (hook_name, args, cb) { + + //serve index.html under / + args.app.get('/', function(req, res) + { + res.send(eejs.require("ep_etherpad-lite/templates/index.html")); + }); + + //serve robots.txt + args.app.get('/robots.txt', function(req, res) + { + var filePath = path.normalize(__dirname + "/../../../static/robots.txt"); + res.sendfile(filePath); + }); + + //serve favicon.ico + args.app.get('/favicon.ico', function(req, res) + { + var filePath = path.normalize(__dirname + "/../../../static/custom/favicon.ico"); + res.sendfile(filePath, function(err) + { + //there is no custom favicon, send the default favicon + if(err) + { + filePath = path.normalize(__dirname + "/../../../static/favicon.ico"); + res.sendfile(filePath); + } + }); + }); + + //serve pad.html under /p + args.app.get('/p/:pad', function(req, res, next) + { + res.send(eejs.require("ep_etherpad-lite/templates/pad.html")); + }); + + //serve timeslider.html under /p/$padname/timeslider + args.app.get('/p/:pad/timeslider', function(req, res, next) + { + res.send(eejs.require("ep_etherpad-lite/templates/timeslider.html")); + }); + +}
\ No newline at end of file diff --git a/src/node/hooks/express/static.js b/src/node/hooks/express/static.js new file mode 100644 index 00000000..f284e478 --- /dev/null +++ b/src/node/hooks/express/static.js @@ -0,0 +1,57 @@ +var path = require('path'); +var minify = require('../../utils/Minify'); +var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins"); +var CachingMiddleware = require('../../utils/caching_middleware'); +var settings = require("../../utils/Settings"); +var Yajsml = require('yajsml'); +var fs = require("fs"); +var ERR = require("async-stacktrace"); +var _ = require("underscore"); + +exports.expressCreateServer = function (hook_name, args, cb) { + // Cache both minified and static. + var assetCache = new CachingMiddleware; + args.app.all('/(javascripts|static)/*', assetCache.handle); + + // Minify will serve static files compressed (minify enabled). It also has + // file-specific hacks for ace/require-kernel/etc. + args.app.all('/static/:filename(*)', minify.minify); + + // Setup middleware that will package JavaScript files served by minify for + // CommonJS loader on the client-side. + var jsServer = new (Yajsml.Server)({ + rootPath: 'javascripts/src/' + , rootURI: 'http://localhost:' + settings.port + '/static/js/' + , libraryPath: 'javascripts/lib/' + , libraryURI: 'http://localhost:' + settings.port + '/static/plugins/' + }); + + var StaticAssociator = Yajsml.associators.StaticAssociator; + var associations = + Yajsml.associators.associationsForSimpleMapping(minify.tar); + var associator = new StaticAssociator(associations); + jsServer.setAssociator(associator); + args.app.use(jsServer); + + // serve plugin definitions + // not very static, but served here so that client can do require("pluginfw/static/js/plugin-definitions.js"); + args.app.get('/pluginfw/plugin-definitions.json', function (req, res, next) { + + var clientParts = _(plugins.parts) + .filter(function(part){ return _(part).has('client_hooks') }); + + var clientPlugins = {}; + + _(clientParts).chain() + .map(function(part){ return part.plugin }) + .uniq() + .each(function(name){ + clientPlugins[name] = _(plugins.plugins[name]).clone(); + delete clientPlugins[name]['package']; + }); + + res.header("Content-Type","application/json; charset=utf-8"); + res.write(JSON.stringify({"plugins": clientPlugins, "parts": clientParts})); + res.end(); + }); +} diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js new file mode 100644 index 00000000..d0e28737 --- /dev/null +++ b/src/node/hooks/express/webaccess.js @@ -0,0 +1,51 @@ +var express = require('express'); +var log4js = require('log4js'); +var httpLogger = log4js.getLogger("http"); +var settings = require('../../utils/Settings'); + + +//checks for basic http auth +exports.basicAuth = function (req, res, next) { + + // When handling HTTP-Auth, an undefined password will lead to no authorization at all + var pass = settings.httpAuth || ''; + + if (req.path.indexOf('/admin') == 0) { + var pass = settings.adminHttpAuth; + + } + + // Just pass if password is an empty string + if (pass === '') { + return next(); + } + + + // If a password has been set and auth headers are present... + if (pass && req.headers.authorization && req.headers.authorization.search('Basic ') === 0) { + // ...check login and password + if (new Buffer(req.headers.authorization.split(' ')[1], 'base64').toString() === pass) { + return next(); + } + } + + // Otherwise return Auth required Headers, delayed for 1 second, if auth failed. + res.header('WWW-Authenticate', 'Basic realm="Protected Area"'); + if (req.headers.authorization) { + setTimeout(function () { + res.send('Authentication required', 401); + }, 1000); + } else { + res.send('Authentication required', 401); + } +} + +exports.expressConfigure = function (hook_name, args, cb) { + args.app.use(exports.basicAuth); + + // If the log level specified in the config file is WARN or ERROR the application server never starts listening to requests as reported in issue #158. + // Not installing the log4js connect logger when the log level has a higher severity than INFO since it would not log at that level anyway. + if (!(settings.loglevel === "WARN" || settings.loglevel == "ERROR")) + args.app.use(log4js.connectLogger(httpLogger, { level: log4js.levels.INFO, format: ':status, :method :url'})); + args.app.use(express.cookieParser()); +} diff --git a/src/node/padaccess.js b/src/node/padaccess.js new file mode 100644 index 00000000..a3d1df33 --- /dev/null +++ b/src/node/padaccess.js @@ -0,0 +1,21 @@ +var ERR = require("async-stacktrace"); +var securityManager = require('./db/SecurityManager'); + +//checks for padAccess +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) { + if(ERR(err, callback)) return; + + //there is access, continue + if(accessObj.accessStatus == "grant") { + callback(); + //no access + } else { + res.send("403 - Can't touch this", 403); + } + }); +} diff --git a/src/node/server.js b/src/node/server.js new file mode 100644 index 00000000..6b443edb --- /dev/null +++ b/src/node/server.js @@ -0,0 +1,99 @@ +/** + * This module is started with bin/run.sh. It sets up a Express HTTP and a Socket.IO Server. + * Static file Requests are answered directly from this module, Socket.IO messages are passed + * to MessageHandler and minfied requests are passed to minified. + */ + +/* + * 2011 Peter 'Pita' Martischka (Primary Technology Ltd) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var log4js = require('log4js'); +var fs = require('fs'); +var settings = require('./utils/Settings'); +var db = require('./db/DB'); +var async = require('async'); +var express = require('express'); +var path = require('path'); +var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins"); +var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); +var npm = require("npm/lib/npm.js"); + +//try to get the git version +var version = ""; +try +{ + var rootPath = path.resolve(npm.dir, '..'); + var ref = fs.readFileSync(rootPath + "/.git/HEAD", "utf-8"); + var refPath = rootPath + "/.git/" + ref.substring(5, ref.indexOf("\n")); + version = fs.readFileSync(refPath, "utf-8"); + version = version.substring(0, 7); + console.log("Your Etherpad Lite git version is " + version); +} +catch(e) +{ + console.warn("Can't get git version for server header\n" + e.message) +} + +console.log("Report bugs at https://github.com/Pita/etherpad-lite/issues") + +var serverName = "Etherpad-Lite " + version + " (http://j.mp/ep-lite)"; + +//set loglevel +log4js.setGlobalLogLevel(settings.loglevel); + +async.waterfall([ + //initalize the database + function (callback) + { + db.init(callback); + }, + + plugins.update, + + function (callback) { + console.log("Installed plugins: " + plugins.formatPlugins()); + console.log("Installed parts:\n" + plugins.formatParts()); + console.log("Installed hooks:\n" + plugins.formatHooks()); + callback(); + }, + + //initalize the http server + function (callback) + { + //create server + var app = express.createServer(); + + app.use(function (req, res, next) { + res.header("Server", serverName); + next(); + }); + + app.configure(function() { hooks.callAll("expressConfigure", {"app": app}); }); + + hooks.callAll("expressCreateServer", {"app": app}); + + //let the server listen + app.listen(settings.port, settings.ip); + console.log("Server is listening at " + settings.ip + ":" + settings.port); + if(settings.adminHttpAuth){ + console.log("Plugin admin page listening at " + settings.ip + ":" + settings.port + "/admin/plugins"); + } + else{ + console.log("Admin username and password not set in settings.json. To access admin please uncomment and edit adminHttpAuth in settings.json"); + } + callback(null); + } +]); diff --git a/node/utils/Abiword.js b/src/node/utils/Abiword.js index 27138e64..27138e64 100644 --- a/node/utils/Abiword.js +++ b/src/node/utils/Abiword.js diff --git a/node/utils/Cli.js b/src/node/utils/Cli.js index 0c7947e9..0c7947e9 100644 --- a/node/utils/Cli.js +++ b/src/node/utils/Cli.js diff --git a/node/utils/ExportDokuWiki.js b/src/node/utils/ExportDokuWiki.js index abe6d347..bcb21108 100644 --- a/node/utils/ExportDokuWiki.js +++ b/src/node/utils/ExportDokuWiki.js @@ -15,8 +15,8 @@ */ var async = require("async"); -var CommonCode = require('./common_code'); -var Changeset = CommonCode.require("/Changeset"); + +var Changeset = require("ep_etherpad-lite/static/js/Changeset"); var padManager = require("../db/PadManager"); function getPadDokuWiki(pad, revNum, callback) diff --git a/node/utils/ExportHtml.js b/src/node/utils/ExportHtml.js index afeafd3a..91ebe59f 100644 --- a/node/utils/ExportHtml.js +++ b/src/node/utils/ExportHtml.js @@ -14,12 +14,12 @@ * limitations under the License. */ -var CommonCode = require('./common_code'); + var async = require("async"); -var Changeset = CommonCode.require("/Changeset"); +var Changeset = require("ep_etherpad-lite/static/js/Changeset"); var padManager = require("../db/PadManager"); var ERR = require("async-stacktrace"); -var Security = CommonCode.require('/security'); +var Security = require('ep_etherpad-lite/static/js/security'); function getPadPlainText(pad, revNum) { diff --git a/node/utils/ImportHtml.js b/src/node/utils/ImportHtml.js index ce866369..4b50b032 100644 --- a/node/utils/ImportHtml.js +++ b/src/node/utils/ImportHtml.js @@ -17,10 +17,10 @@ var jsdom = require('jsdom-nocontextifiy').jsdom; var log4js = require('log4js'); -var CommonCode = require('../utils/common_code'); -var Changeset = CommonCode.require("/Changeset"); -var contentcollector = CommonCode.require("/contentcollector"); -var map = CommonCode.require("/ace2_common").map; + +var Changeset = require("ep_etherpad-lite/static/js/Changeset"); +var contentcollector = require("ep_etherpad-lite/static/js/contentcollector"); +var map = require("ep_etherpad-lite/static/js/ace2_common").map; function setPadHTML(pad, html, callback) { diff --git a/node/utils/Minify.js b/src/node/utils/Minify.js index 39c6ceb3..c5996565 100644 --- a/node/utils/Minify.js +++ b/src/node/utils/Minify.js @@ -27,19 +27,22 @@ var cleanCSS = require('clean-css'); var jsp = require("uglify-js").parser; var pro = require("uglify-js").uglify; var path = require('path'); +var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins"); var RequireKernel = require('require-kernel'); -var server = require('../server'); var ROOT_DIR = path.normalize(__dirname + "/../../static/"); var TAR_PATH = path.join(__dirname, 'tar.json'); var tar = JSON.parse(fs.readFileSync(TAR_PATH, 'utf8')); // Rewrite tar to include modules with no extensions and proper rooted paths. +var LIBRARY_PREFIX = 'ep_etherpad-lite/static/js'; exports.tar = {}; for (var key in tar) { - exports.tar['/' + key] = - tar[key].map(function (p) {return '/' + p}).concat( - tar[key].map(function (p) {return '/' + p.replace(/\.js$/, '')}) + exports.tar[LIBRARY_PREFIX + '/' + key] = + tar[key].map(function (p) {return LIBRARY_PREFIX + '/' + p}).concat( + tar[key].map(function (p) { + return LIBRARY_PREFIX + '/' + p.replace(/\.js$/, '') + }) ); } @@ -63,6 +66,22 @@ exports.minify = function(req, res, next) return; } + /* Handle static files for plugins: + paths like "plugins/ep_myplugin/static/js/test.js" + are rewritten into ROOT_PATH_OF_MYPLUGIN/static/js/test.js, + commonly ETHERPAD_ROOT/node_modules/ep_myplugin/static/js/test.js + */ + var match = filename.match(/^plugins\/([^\/]+)\/static\/(.*)/); + if (match) { + var pluginName = match[1]; + var resourcePath = match[2]; + var plugin = plugins.plugins[pluginName]; + if (plugin) { + var pluginPath = plugin.package.realPath; + filename = path.relative(ROOT_DIR, pluginPath + '/static/' + resourcePath); + } + } + // What content type should this be? // TODO: This should use a MIME module. var contentType; @@ -89,10 +108,10 @@ exports.minify = function(req, res, next) date = new Date(date); res.setHeader('last-modified', date.toUTCString()); res.setHeader('date', (new Date()).toUTCString()); - if (server.maxAge) { - var expiresDate = new Date((new Date()).getTime()+server.maxAge*1000); + if (settings.maxAge !== undefined) { + var expiresDate = new Date((new Date()).getTime()+settings.maxAge*1000); res.setHeader('expires', expiresDate.toUTCString()); - res.setHeader('cache-control', 'max-age=' + server.maxAge); + res.setHeader('cache-control', 'max-age=' + settings.maxAge); } } @@ -112,7 +131,10 @@ exports.minify = function(req, res, next) res.end(); } else if (req.method == 'GET') { getFileCompressed(filename, contentType, function (error, content) { - if(ERR(error)) return; + if(ERR(error, function(){ + res.writeHead(500, {}); + res.end(); + })) return; res.header("Content-Type", contentType); res.writeHead(200, {}); res.write(content); diff --git a/src/node/utils/Minify.js.rej b/src/node/utils/Minify.js.rej new file mode 100644 index 00000000..f09f49dc --- /dev/null +++ b/src/node/utils/Minify.js.rej @@ -0,0 +1,513 @@ +/** + * This Module manages all /minified/* requests. It controls the + * minification && compression of Javascript and CSS. + */ + +/* + * 2011 Peter 'Pita' Martischka (Primary Technology Ltd) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var ERR = require("async-stacktrace"); +var settings = require('./Settings'); +var async = require('async'); +var fs = require('fs'); +var cleanCSS = require('clean-css'); +var jsp = require("uglify-js").parser; +var pro = require("uglify-js").uglify; +var path = require('path'); +var RequireKernel = require('require-kernel'); +var server = require('../server'); + +<<<<<<< HEAD +var ROOT_DIR = path.normalize(__dirname + "/../" ); +var JS_DIR = ROOT_DIR + '../static/js/'; +var CSS_DIR = ROOT_DIR + '../static/css/'; +var CACHE_DIR = path.join(settings.root, 'var'); +======= +var ROOT_DIR = path.normalize(__dirname + "/../../static/"); +>>>>>>> pita +var TAR_PATH = path.join(__dirname, 'tar.json'); +var tar = JSON.parse(fs.readFileSync(TAR_PATH, 'utf8')); + +// Rewrite tar to include modules with no extensions and proper rooted paths. +exports.tar = {}; +for (var key in tar) { + exports.tar['/' + key] = + tar[key].map(function (p) {return '/' + p}).concat( + tar[key].map(function (p) {return '/' + p.replace(/\.js$/, '')}) + ); +} + +/** + * creates the minifed javascript for the given minified name + * @param req the Express request + * @param res the Express response + */ +exports.minify = function(req, res, next) +{ +<<<<<<< HEAD + var jsFilename = req.params[0]; + + //choose the js files we need + var jsFiles = undefined; + if (Object.prototype.hasOwnProperty.call(tar, jsFilename)) { + jsFiles = tar[jsFilename]; + } else { + /* Not in tar list, but try anyways, if it fails, pass to `next`. + Actually try, not check in filesystem here because + we don't want to duplicate the require.resolve() handling + */ + jsFiles = [jsFilename]; + } + _handle(req, res, jsFilename, jsFiles, function (err) { + console.log("Unable to load minified file " + jsFilename + ": " + err.toString()); + /* Throw away error and generate a 404, not 500 */ + next(); + }); +} + +function _handle(req, res, jsFilename, jsFiles, next) { + res.header("Content-Type","text/javascript"); + + var cacheName = CACHE_DIR + "/minified_" + jsFilename.replace(/\//g, "_"); + + //minifying is enabled + if(settings.minify) + { + var result = undefined; + var latestModification = 0; + + async.series([ + //find out the highest modification date + function(callback) + { + var folders2check = [CSS_DIR, JS_DIR]; + + //go trough this two folders + async.forEach(folders2check, function(path, callback) + { + //read the files in the folder + fs.readdir(path, function(err, files) + { + if(ERR(err, callback)) return; + + //we wanna check the directory itself for changes too + files.push("."); + + //go trough all files in this folder + async.forEach(files, function(filename, callback) + { + //get the stat data of this file + fs.stat(path + "/" + filename, function(err, stats) + { + if(ERR(err, callback)) return; + + //get the modification time + var modificationTime = stats.mtime.getTime(); + + //compare the modification time to the highest found + if(modificationTime > latestModification) + { + latestModification = modificationTime; + } + + callback(); + }); + }, callback); + }); + }, callback); + }, + function(callback) + { + //check the modification time of the minified js + fs.stat(cacheName, function(err, stats) + { + if(err && err.code != "ENOENT") + { + ERR(err, callback); + return; + } + + //there is no minfied file or there new changes since this file was generated, so continue generating this file + if((err && err.code == "ENOENT") || stats.mtime.getTime() < latestModification) + { + callback(); + } + //the minified file is still up to date, stop minifying + else + { + callback("stop"); + } + }); + }, + //load all js files + function (callback) + { + var values = []; + tarCode( + jsFiles + , function (content) {values.push(content)} + , function (err) { + if(ERR(err, next)) return; + + result = values.join(''); + callback(); + }); + }, + //put all together and write it into a file + function(callback) + { + async.parallel([ + //write the results plain in a file + function(callback) + { + fs.writeFile(cacheName, result, "utf8", callback); + }, + //write the results compressed in a file + function(callback) + { + zlib.gzip(result, function(err, compressedResult){ + //weird gzip bug that returns 0 instead of null if everything is ok + err = err === 0 ? null : err; + + if(ERR(err, callback)) return; + + fs.writeFile(cacheName + ".gz", compressedResult, callback); + }); + } + ],callback); + } + ], function(err) + { + if(err && err != "stop") + { + if(ERR(err)) return; + } + + //check if gzip is supported by this browser + var gzipSupport = req.header('Accept-Encoding', '').indexOf('gzip') != -1; + + var pathStr; + if(gzipSupport && os.type().indexOf("Windows") == -1) + { + pathStr = path.normalize(cacheName + ".gz"); + res.header('Content-Encoding', 'gzip'); + } + else + { + pathStr = path.normalize(cacheName); + } + + res.sendfile(pathStr, { maxAge: server.maxAge }); + }) + } + //minifying is disabled, so put the files together in one file + else + { + tarCode( + jsFiles + , function (content) {res.write(content)} + , function (err) { + if(ERR(err, next)) return; +======= + var filename = req.params['filename']; + + // No relative paths, especially if they may go up the file hierarchy. + filename = path.normalize(path.join(ROOT_DIR, filename)); + if (filename.indexOf(ROOT_DIR) == 0) { + filename = filename.slice(ROOT_DIR.length); + filename = filename.replace(/\\/g, '/'); // Windows (safe generally?) + } else { + res.writeHead(404, {}); + res.end(); + return; + } + + // What content type should this be? + // TODO: This should use a MIME module. + var contentType; + if (filename.match(/\.js$/)) { + contentType = "text/javascript"; + } else if (filename.match(/\.css$/)) { + contentType = "text/css"; + } else if (filename.match(/\.html$/)) { + contentType = "text/html"; + } else if (filename.match(/\.txt$/)) { + contentType = "text/plain"; + } else if (filename.match(/\.png$/)) { + contentType = "image/png"; + } else if (filename.match(/\.gif$/)) { + contentType = "image/gif"; + } else if (filename.match(/\.ico$/)) { + contentType = "image/x-icon"; + } else { + contentType = "application/octet-stream"; + } + + statFile(filename, function (error, date, exists) { + if (date) { + date = new Date(date); + res.setHeader('last-modified', date.toUTCString()); + res.setHeader('date', (new Date()).toUTCString()); + if (server.maxAge) { + var expiresDate = new Date((new Date()).getTime()+server.maxAge*1000); + res.setHeader('expires', expiresDate.toUTCString()); + res.setHeader('cache-control', 'max-age=' + server.maxAge); + } + } + + if (error) { + res.writeHead(500, {}); +>>>>>>> pita + res.end(); + } else if (!exists) { + res.writeHead(404, {}); + res.end(); + } else if (new Date(req.headers['if-modified-since']) >= date) { + res.writeHead(304, {}); + res.end(); + } else { + if (req.method == 'HEAD') { + res.header("Content-Type", contentType); + res.writeHead(200, {}); + res.end(); + } else if (req.method == 'GET') { + getFileCompressed(filename, contentType, function (error, content) { + if(ERR(error)) return; + res.header("Content-Type", contentType); + res.writeHead(200, {}); + res.write(content); + res.end(); + }); + } else { + res.writeHead(405, {'allow': 'HEAD, GET'}); + res.end(); + } + } + }); +} + +// find all includes in ace.js and embed them. +function getAceFile(callback) { + fs.readFile(ROOT_DIR + 'js/ace.js', "utf8", function(err, data) { + if(ERR(err, callback)) return; + + // Find all includes in ace.js and embed them + var founds = data.match(/\$\$INCLUDE_[a-zA-Z_]+\("[^"]*"\)/gi); + if (!settings.minify) { + founds = []; + } + // Always include the require kernel. + founds.push('$$INCLUDE_JS("../static/js/require-kernel.js")'); + + data += ';\n'; + data += 'Ace2Editor.EMBEDED = Ace2Editor.EMBEDED || {};\n'; + + // Request the contents of the included file on the server-side and write + // them into the file. + async.forEach(founds, function (item, callback) { + var filename = item.match(/"([^"]*)"/)[1]; + var request = require('request'); + + var baseURI = 'http://localhost:' + settings.port + + request(baseURI + path.normalize(path.join('/static/', filename)), function (error, response, body) { + if (!error && response.statusCode == 200) { + data += 'Ace2Editor.EMBEDED[' + JSON.stringify(filename) + '] = ' + + JSON.stringify(body || '') + ';\n'; + } else { + // Silence? + } + callback(); + }); + }, function(error) { + callback(error, data); + }); + }); +} + +// Check for the existance of the file and get the last modification date. +function statFile(filename, callback) { + if (filename == 'js/ace.js') { + // Sometimes static assets are inlined into this file, so we have to stat + // everything. + lastModifiedDateOfEverything(function (error, date) { + callback(error, date, !error); + }); + } else if (filename == 'js/require-kernel.js') { + callback(null, requireLastModified(), true); + } else { + fs.stat(ROOT_DIR + filename, function (error, stats) { + if (error) { + if (error.code == "ENOENT") { + // Stat the directory instead. + fs.stat(path.dirname(ROOT_DIR + filename), function (error, stats) { + if (error) { + if (error.code == "ENOENT") { + callback(null, null, false); + } else { + callback(error); + } + } else { + callback(null, stats.mtime.getTime(), false); + } + }); + } else { + callback(error); + } + } else { + callback(null, stats.mtime.getTime(), true); + } + }); + } +} +function lastModifiedDateOfEverything(callback) { + var folders2check = [ROOT_DIR + 'js/', ROOT_DIR + 'css/']; + var latestModification = 0; + //go trough this two folders + async.forEach(folders2check, function(path, callback) + { + //read the files in the folder + fs.readdir(path, function(err, files) + { + if(ERR(err, callback)) return; + + //we wanna check the directory itself for changes too + files.push("."); + + //go trough all files in this folder + async.forEach(files, function(filename, callback) + { + //get the stat data of this file + fs.stat(path + "/" + filename, function(err, stats) + { + if(ERR(err, callback)) return; + + //get the modification time + var modificationTime = stats.mtime.getTime(); + + //compare the modification time to the highest found + if(modificationTime > latestModification) + { + latestModification = modificationTime; + } + + callback(); + }); + }, callback); + }); + }, function () { + callback(null, latestModification); + }); +} + +// This should be provided by the module, but until then, just use startup +// time. +var _requireLastModified = new Date(); +function requireLastModified() { + return _requireLastModified.toUTCString(); +} +function requireDefinition() { + return 'var require = ' + RequireKernel.kernelSource + ';\n'; +} + +<<<<<<< HEAD +function tarCode(jsFiles, write, callback) { + write('require.define({'); + var initialEntry = true; + async.forEach(jsFiles, function (filename, callback){ + var path; + var srcPath; + if (filename.indexOf('plugins/') == 0) { + srcPath = filename.substring('plugins/'.length); + path = require.resolve(srcPath); + } else { + srcPath = '/' + filename; + path = JS_DIR + filename; + } + + srcPath = JSON.stringify(srcPath); + var srcPathAbbv = JSON.stringify(srcPath.replace(/\.js$/, '')); + + if (filename == 'ace.js') { + getAceFile(handleFile); + } else { + fs.readFile(path, "utf8", handleFile); + } + + function handleFile(err, data) { + if(ERR(err, callback)) return; + + if (!initialEntry) { + write('\n,'); + } else { + initialEntry = false; + } + write(srcPath + ': ') + data = '(function (require, exports, module) {' + data + '})'; +======= +function getFileCompressed(filename, contentType, callback) { + getFile(filename, function (error, content) { + if (error || !content) { + callback(error, content); + } else { +>>>>>>> pita + if (settings.minify) { + if (contentType == 'text/javascript') { + try { + content = compressJS([content]); + } catch (error) { + // silence + } + } else if (contentType == 'text/css') { + content = compressCSS([content]); + } + } + callback(null, content); + } +<<<<<<< HEAD + }, function (err) { + if(ERR(err, callback)) return; + write('});\n'); + callback(); +======= +>>>>>>> pita + }); +} + +function getFile(filename, callback) { + if (filename == 'js/ace.js') { + getAceFile(callback); + } else if (filename == 'js/require-kernel.js') { + callback(undefined, requireDefinition()); + } else { + fs.readFile(ROOT_DIR + filename, callback); + } +} + +function compressJS(values) +{ + var complete = values.join("\n"); + var ast = jsp.parse(complete); // parse code and get the initial AST + ast = pro.ast_mangle(ast); // get a new AST with mangled names + ast = pro.ast_squeeze(ast); // get an AST with compression optimizations + return pro.gen_code(ast); // compressed code here +} + +function compressCSS(values) +{ + var complete = values.join("\n"); + return cleanCSS.process(complete); +} diff --git a/node/utils/Settings.js b/src/node/utils/Settings.js index 0d30fb68..12fcc55c 100644 --- a/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -23,6 +23,10 @@ var fs = require("fs"); var os = require("os"); var path = require('path'); var argv = require('./Cli').argv; +var npm = require("npm/lib/npm.js"); + +/* Root path of the installation */ +exports.root = path.normalize(path.join(npm.dir, "..")); /** * The IP ep-lite should listen to @@ -40,7 +44,7 @@ exports.dbType = "dirty"; /** * This setting is passed with dbType to ueberDB to set up the database */ -exports.dbSettings = { "filename" : "../var/dirty.db" }; +exports.dbSettings = { "filename" : path.join(exports.root, "var/dirty.db") }; /** * The default Text of a new pad */ @@ -81,6 +85,11 @@ exports.loglevel = "INFO"; */ exports.httpAuth = null; +/** + * Http basic auth, with "user:password" format + */ +exports.adminHttpAuth = null; + //checks if abiword is avaiable exports.abiwordAvailable = function() { @@ -96,10 +105,12 @@ exports.abiwordAvailable = function() // Discover where the settings file lives var settingsFilename = argv.settings || "settings.json"; -var settingsPath = settingsFilename.charAt(0) == '/' ? '' : path.normalize(__dirname + "/../../"); +if (settingsFilename.charAt(0) != '/') { + settingsFilename = path.normalize(path.join(root, settingsFilename)); +} //read the settings sync -var settingsStr = fs.readFileSync(settingsPath + settingsFilename).toString(); +var settingsStr = fs.readFileSync(settingsFilename).toString(); //remove all comments settingsStr = settingsStr.replace(/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/gm,"").replace(/#.*/g,"").replace(/\/\/.*/g,""); diff --git a/node/utils/caching_middleware.js b/src/node/utils/caching_middleware.js index a26e22d1..70d5a08c 100644 --- a/node/utils/caching_middleware.js +++ b/src/node/utils/caching_middleware.js @@ -18,12 +18,12 @@ var async = require('async'); var Buffer = require('buffer').Buffer; var fs = require('fs'); var path = require('path'); -var server = require('../server'); var zlib = require('zlib'); var util = require('util'); +var settings = require('./Settings'); -var ROOT_DIR = path.normalize(__dirname + "/../"); -var CACHE_DIR = ROOT_DIR + '../var/'; +var CACHE_DIR = path.normalize(path.join(settings.root, 'var/')); +CACHE_DIR = path.existsSync(CACHE_DIR) ? CACHE_DIR : undefined; var responseCache = {}; @@ -37,7 +37,7 @@ function CachingMiddleware() { } CachingMiddleware.prototype = new function () { function handle(req, res, next) { - if (!(req.method == "GET" || req.method == "HEAD")) { + if (!(req.method == "GET" || req.method == "HEAD") || !CACHE_DIR) { return next(undefined, req, res); } diff --git a/node/utils/customError.js b/src/node/utils/customError.js index 5ca7a7a4..5ca7a7a4 100644 --- a/node/utils/customError.js +++ b/src/node/utils/customError.js diff --git a/src/node/utils/randomstring.js b/src/node/utils/randomstring.js new file mode 100644 index 00000000..4c1bba24 --- /dev/null +++ b/src/node/utils/randomstring.js @@ -0,0 +1,16 @@ +/** + * Generates a random String with the given length. Is needed to generate the Author, Group, readonly, session Ids + */ +var 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; +}; + +module.exports = randomString; diff --git a/node/utils/tar.json b/src/node/utils/tar.json index e922ddde..5c1abac5 100644 --- a/node/utils/tar.json +++ b/src/node/utils/tar.json @@ -1,11 +1,11 @@ { "pad.js": [ "jquery.js" + , "underscore.js" , "security.js" , "pad.js" , "ace2_common.js" , "pad_utils.js" - , "plugins.js" , "undo-xpopup.js" , "json2.js" , "pad_cookie.js" @@ -22,12 +22,11 @@ , "chat.js" , "excanvas.js" , "farbtastic.js" - , "prefixfree.js" ] , "timeslider.js": [ "jquery.js" + , "underscore.js" , "security.js" - , "plugins.js" , "undo-xpopup.js" , "json2.js" , "colorutils.js" @@ -41,7 +40,7 @@ , "pad_modals.js" , "pad_savedrevs.js" , "pad_impexp.js" - , "AttributePoolFactory.js" + , "AttributePool.js" , "Changeset.js" , "domline.js" , "linestylefilter.js" @@ -53,8 +52,11 @@ ] , "ace2_inner.js": [ "ace2_common.js" - , "AttributePoolFactory.js" + , "underscore.js" + , "rjquery.js" + , "AttributePool.js" , "Changeset.js" + , "ChangesetUtils.js" , "security.js" , "skiplist.js" , "virtual_lines.js" @@ -65,6 +67,7 @@ , "changesettracker.js" , "linestylefilter.js" , "domline.js" + , "AttributeManager.js" , "ace2_inner.js" ] } diff --git a/package.json b/src/package.json index 01eb8e96..83441da0 100644 --- a/package.json +++ b/src/package.json @@ -1,5 +1,5 @@ { - "name" : "etherpad-lite", + "name" : "ep_etherpad-lite", "description" : "A Etherpad based on node.js", "homepage" : "https://github.com/Pita/etherpad-lite", "keywords" : ["etherpad", "realtime", "collaborative", "editor"], @@ -12,7 +12,7 @@ "dependencies" : { "yajsml" : "1.1.2", "request" : "2.9.100", - "require-kernel" : "1.0.3", + "require-kernel" : "1.0.5", "socket.io" : "0.8.7", "ueberDB" : "0.1.7", "async" : "0.1.18", @@ -22,7 +22,13 @@ "formidable" : "1.0.9", "log4js" : "0.4.1", "jsdom-nocontextifiy" : "0.2.10", - "async-stacktrace" : "0.0.2" + "async-stacktrace" : "0.0.2", + "npm" : "1.1", + "ejs" : "0.6.1", + "graceful-fs" : "1.1.5", + "slide" : "1.1.3", + "semver" : "1.0.13", + "underscore" : "1.3.1" }, "devDependencies": { "jshint" : "*" diff --git a/src/static/css/admin.css b/src/static/css/admin.css new file mode 100644 index 00000000..80089c4c --- /dev/null +++ b/src/static/css/admin.css @@ -0,0 +1,49 @@ +table { + border-collapse: collapse; +} +td, th { + border: 1px solid black; + padding-left: 10px; + padding-right: 10px; + padding-top: 2px; + padding-bottom: 2px; +} +.template { + display: none; +} +.dialog { + display: none; + position: absolute; + left: 50%; + top: 50%; + width: 700px; + height: 500px; + margin-left: -350px; + margin-top: -250px; + border: 3px solid #999999; + background: #eeeeee; +} +.dialog .title { + margin: 0; + padding: 2px; + border-bottom: 3px solid #999999; + font-size: 24px; + line-height: 24px; + height: 24px; + overflow: hidden; +} +.dialog .title .close { + float: right; +} +.dialog .history { + background: #222222; + color: #eeeeee; + position: absolute; + top: 41px; + bottom: 10px; + left: 10px; + right: 10px; + padding: 2px; + overflow: auto; +} + diff --git a/static/css/iframe_editor.css b/src/static/css/iframe_editor.css index d2d2f977..d2d2f977 100644 --- a/static/css/iframe_editor.css +++ b/src/static/css/iframe_editor.css diff --git a/static/css/pad.css b/src/static/css/pad.css index 969d0027..21f365e2 100644 --- a/static/css/pad.css +++ b/src/static/css/pad.css @@ -24,7 +24,7 @@ a img } /* menu */ -#editbar ul +.toolbar ul { position: relative; list-style: none; @@ -35,18 +35,19 @@ a img } -#editbar +.toolbar { background: #f7f7f7; background: linear-gradient(#f7f7f7, #f1f1f1 80%); border-bottom: 1px solid #ccc; - height: 32px; overflow: hidden; padding-top: 3px; width: 100%; + white-space: nowrap; + height: 32px; } -#editbar ul li +.toolbar ul li { background: #fff; background: linear-gradient(#fff, #f0f0f0); @@ -61,52 +62,52 @@ a img width: 18px; } -#editbar ul li a +.toolbar ul li a { text-decoration: none; color: #ccc; position: absolute; } -#editbar ul li a span +.toolbar ul li a span { position: relative; top:-2px } -#editbar ul li:hover { +.toolbar ul li:hover { background: #fff; background: linear-gradient(#f4f4f4, #e4e4e4); } -#editbar ul li:active { +.toolbar ul li:active { background: #eee; background: linear-gradient(#ddd, #fff); box-shadow: 0 0 8px rgba(0,0,0,.1) inset; } -#editbar ul li.separator +.toolbar ul li.separator { border: inherit; background: inherit; visibility:hidden; width: 0px; } -#editbar ul li a +.toolbar ul li a { display: block; } -#editbar ul li a img +.toolbar ul li a img { padding: 1px; } -#editbar ul +.toolbar ul { float: left; } -#editbar ul#menu_right +.toolbar ul.menu_right { float: right; } @@ -175,7 +176,6 @@ a#backtoprosite { padding-left: 20px; left: 6px; background: url(static/img/protop.gif) no-repeat -5px -6px; } #accountnav { right: 30px; color: #fff; } -.propad a#topbaretherpad { background: url(static/img/protop.gif) no-repeat -397px -3px; } #specialkeyarea { top: 5px; left: 250px; color: yellow; font-weight: bold; font-size: 1.5em; position: absolute; } @@ -320,7 +320,7 @@ a#hidetopmsg { position: absolute; right: 5px; bottom: 5px; } z-index: 10; } -#editbarsavetable +.toolbarsavetable { position:absolute; top: 6px; @@ -328,7 +328,7 @@ a#hidetopmsg { position: absolute; right: 5px; bottom: 5px; } height: 24px; } -#editbarsavetable td, #editbartable td +.toolbarsavetable td, .toolbartable td { white-space: nowrap; } @@ -604,8 +604,6 @@ table#otheruserstable { display: none; } text-align: left; } -.nonprouser #sharebox-stripe { display: none; } - .sharebox-url { width: 440px; height: 18px; text-align: left; @@ -688,14 +686,15 @@ a#topbarmaximize { background: url(static/img/maximize_maximized.png); } -#editbarinner h1 { +.toolbarinner h1 { line-height: 29px; font-size: 16px; padding-left: 6pt; margin-top: 0; + white-space: nowrap; } -#editbarinner h1 a { +.toolbarinner h1 a { font-size: 12px; } @@ -1034,6 +1033,9 @@ margin-top: 1px; background-position: 0px -183px; display: inline-block; } +.buttonicon-savedRevision { + background-position: 0px -493px +} #usericon { @@ -1173,13 +1175,13 @@ input[type=checkbox] { } @media screen and (max-width: 600px) { - #editbar ul li { + .toolbar ul li { padding: 4px 1px; } } @media only screen and (min-device-width: 320px) and (max-device-width: 720px) { - #editbar ul li { + .toolbar ul li { padding: 4px 3px; } #users { @@ -1194,7 +1196,7 @@ input[type=checkbox] { #editorcontainer { margin-bottom: 33px; } - #editbar ul#menu_right { + .toolbar ul.menu_right { background: #f7f7f7; background: linear-gradient(#f7f7f7, #f1f1f1 80%); width: 100%; @@ -1204,7 +1206,7 @@ input[type=checkbox] { bottom: 0; border-top: 1px solid #ccc; } - #editbar ul#menu_right li:last-child { + .toolbar ul.menu_right li:last-child { height: 24px; border-radius: 0; margin-top: 0; @@ -1226,7 +1228,7 @@ input[type=checkbox] { border-top-right-radius: 0; border-right: none; } - #editbar ul li a span { + .toolbar ul li a span { top: -3px; } #usericonback { @@ -1235,10 +1237,10 @@ input[type=checkbox] { #qrcode { display: none; } - #editbar ul#menu_right li:not(:last-child) { + .toolbar ul.menu_right li:not(:last-child) { display: block; } - #editbar ul#menu_right > li { + .toolbar ul.menu_right > li { background: none; border: none; margin-top: 4px; @@ -1267,4 +1269,4 @@ input[type=checkbox] { #online_count { line-height: 24px; } -}
\ No newline at end of file +} diff --git a/static/css/timeslider.css b/src/static/css/timeslider.css index 926c8012..03e97048 100644 --- a/static/css/timeslider.css +++ b/src/static/css/timeslider.css @@ -1,4 +1,7 @@ -#editorcontainerbox {overflow:auto; top:40px;} +#editorcontainerbox { + overflow:auto; top:40px; + position: static; +} #padcontent {font-size:12px; padding:10px;} @@ -42,10 +45,10 @@ #leftstar, #rightstar, #leftstep, #rightstep {background:url(../../static/img/stepper_buttons.png) 0 0 no-repeat; height:21px; overflow:hidden; position:absolute;} -#leftstar {background-position:0 44px; right:34px; top:8px; width:30px;} -#rightstar {background-position:29px 44px; right:5px; top:8px; width:29px;} -#leftstep {background-position:0 22px; right:34px; top:20px; width:30px;} -#rightstep {background-position:29px 22px; right:5px; top:20px; width:29px;} +#leftstar {background-position:0 -44px; right:34px; top:8px; width:30px;} +#rightstar {background-position:-29px -44px; right:5px; top:8px; width:29px;} +#leftstep {background-position:0 -22px; right:34px; top:20px; width:30px;} +#rightstep {background-position:-29px -22px; right:5px; top:20px; width:29px;} #timeslider .star { background-image:url(../../static/img/star.png); @@ -67,12 +70,63 @@ width:122px; } + .topbarcenter, #docbar {display:none;} -#padmain {top:30px;} +#padmain {top:0px !important;} #editbarright {float:right;} #returnbutton {color:#222; font-size:16px; line-height:29px; margin-top:0; padding-right:6px;} -#importexport {top:118px;} #importexport .popup {width:185px;} +#importexport{ + top:118px; + width:185px; +} + + +.timeslider-bar +{ + background: #f7f7f7; + background: linear-gradient(#f7f7f7, #f1f1f1 80%); + border-bottom: 1px solid #ccc; + overflow: hidden; + padding-top: 3px; + width: 100%; +} + +.timeslider-bar #editbar +{ + border-bottom: none; + float: right; + width: 170px; + width: initial; +} + +.timeslider-bar h1 +{ + margin: 5px; +} +.timeslider-bar p +{ + margin: 5px; +} +#timeslider-top { + width: 100%; + position: fixed; + z-index: 1; +} + +#authorsList .author { + padding-left: 0.4em; + padding-right: 0.4em; +} + +#authorsList .author-anonymous { + padding-left: 0.6em; + padding-right: 0.6em; +} + +#padeditor { + position: static; +} /* lists */ .list-bullet2, .list-indent2, .list-number2 {margin-left:3em;} diff --git a/src/static/custom/.gitignore b/src/static/custom/.gitignore new file mode 100644 index 00000000..aae16bb2 --- /dev/null +++ b/src/static/custom/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!*.template diff --git a/static/custom/css.template b/src/static/custom/css.template index 236251d9..236251d9 100644 --- a/static/custom/css.template +++ b/src/static/custom/css.template diff --git a/static/custom/js.template b/src/static/custom/js.template index 152c3d5d..152c3d5d 100644 --- a/static/custom/js.template +++ b/src/static/custom/js.template diff --git a/static/favicon.ico b/src/static/favicon.ico Binary files differindex df7b6289..df7b6289 100644 --- a/static/favicon.ico +++ b/src/static/favicon.ico diff --git a/static/img/backgrad.gif b/src/static/img/backgrad.gif Binary files differindex 8fee1a5b..8fee1a5b 100644 --- a/static/img/backgrad.gif +++ b/src/static/img/backgrad.gif diff --git a/static/img/connectingbar.gif b/src/static/img/connectingbar.gif Binary files differindex 34f54e90..34f54e90 100644 --- a/static/img/connectingbar.gif +++ b/src/static/img/connectingbar.gif diff --git a/static/img/crushed_button_depressed.png b/src/static/img/crushed_button_depressed.png Binary files differindex d75dcce2..d75dcce2 100644 --- a/static/img/crushed_button_depressed.png +++ b/src/static/img/crushed_button_depressed.png diff --git a/static/img/crushed_button_undepressed.png b/src/static/img/crushed_button_undepressed.png Binary files differindex d86e3f39..d86e3f39 100644 --- a/static/img/crushed_button_undepressed.png +++ b/src/static/img/crushed_button_undepressed.png diff --git a/static/img/crushed_current_location.png b/src/static/img/crushed_current_location.png Binary files differindex 76e08359..76e08359 100644 --- a/static/img/crushed_current_location.png +++ b/src/static/img/crushed_current_location.png diff --git a/src/static/img/etherpad_lite_icons.png b/src/static/img/etherpad_lite_icons.png Binary files differnew file mode 100644 index 00000000..27867d42 --- /dev/null +++ b/src/static/img/etherpad_lite_icons.png diff --git a/static/img/fileicons.gif b/src/static/img/fileicons.gif Binary files differindex c03b6031..c03b6031 100644 --- a/static/img/fileicons.gif +++ b/src/static/img/fileicons.gif diff --git a/static/img/leftarrow.png b/src/static/img/leftarrow.png Binary files differindex 1bec1288..1bec1288 100644 --- a/static/img/leftarrow.png +++ b/src/static/img/leftarrow.png diff --git a/static/img/loading.gif b/src/static/img/loading.gif Binary files differindex bb42be59..bb42be59 100644 --- a/static/img/loading.gif +++ b/src/static/img/loading.gif diff --git a/static/img/pause.png b/src/static/img/pause.png Binary files differindex 657782c0..657782c0 100644 --- a/static/img/pause.png +++ b/src/static/img/pause.png diff --git a/static/img/play.png b/src/static/img/play.png Binary files differindex 19afe034..19afe034 100644 --- a/static/img/play.png +++ b/src/static/img/play.png diff --git a/static/img/roundcorner_left.gif b/src/static/img/roundcorner_left.gif Binary files differindex 000de752..000de752 100644 --- a/static/img/roundcorner_left.gif +++ b/src/static/img/roundcorner_left.gif diff --git a/static/img/roundcorner_right.gif b/src/static/img/roundcorner_right.gif Binary files differindex 97acfbf2..97acfbf2 100644 --- a/static/img/roundcorner_right.gif +++ b/src/static/img/roundcorner_right.gif diff --git a/src/static/img/star.png b/src/static/img/star.png Binary files differnew file mode 100644 index 00000000..e0c7099e --- /dev/null +++ b/src/static/img/star.png diff --git a/static/img/stepper_buttons.png b/src/static/img/stepper_buttons.png Binary files differindex e011a451..e011a451 100644 --- a/static/img/stepper_buttons.png +++ b/src/static/img/stepper_buttons.png diff --git a/static/img/timeslider_background.png b/src/static/img/timeslider_background.png Binary files differindex 851af4e8..851af4e8 100644 --- a/static/img/timeslider_background.png +++ b/src/static/img/timeslider_background.png diff --git a/static/img/timeslider_left.png b/src/static/img/timeslider_left.png Binary files differindex 48a9b0e1..48a9b0e1 100644 --- a/static/img/timeslider_left.png +++ b/src/static/img/timeslider_left.png diff --git a/static/img/timeslider_right.png b/src/static/img/timeslider_right.png Binary files differindex 1a1b2685..1a1b2685 100644 --- a/static/img/timeslider_right.png +++ b/src/static/img/timeslider_right.png diff --git a/src/static/js/AttributeManager.js b/src/static/js/AttributeManager.js new file mode 100644 index 00000000..2d523f6a --- /dev/null +++ b/src/static/js/AttributeManager.js @@ -0,0 +1,164 @@ +var Changeset = require('./Changeset'); +var ChangesetUtils = require('./ChangesetUtils'); +var _ = require('./underscore'); + +var lineMarkerAttribute = 'lmkr'; + +// If one of these attributes are set to the first character of a +// line it is considered as a line attribute marker i.e. attributes +// set on this marker are applied to the whole line. +// The list attribute is only maintained for compatibility reasons +var lineAttributes = [lineMarkerAttribute,'list']; + +/* + The Attribute manager builds changesets based on a document + representation for setting and removing range or line-based attributes. + + @param rep the document representation to be used + @param applyChangesetCallback this callback will be called + once a changeset has been built. + + + A document representation contains + - an array `alines` containing 1 attributes string for each line + - an Attribute pool `apool` + - a SkipList `lines` containing the text lines of the document. +*/ + +var AttributeManager = function(rep, applyChangesetCallback) +{ + this.rep = rep; + this.applyChangesetCallback = applyChangesetCallback; + this.author = ''; + + // If the first char in a line has one of the following attributes + // it will be considered as a line marker +}; + +AttributeManager.lineAttributes = lineAttributes; + +AttributeManager.prototype = _(AttributeManager.prototype).extend({ + + applyChangeset: function(changeset){ + if(!this.applyChangesetCallback) return changeset; + + var cs = changeset.toString(); + if (!Changeset.isIdentity(cs)) + { + this.applyChangesetCallback(cs); + } + + return changeset; + }, + + /* + Sets attributes on a range + @param start [row, col] tuple pointing to the start of the range + @param end [row, col] tuple pointing to the end of the range + @param attribute: an array of attributes + */ + setAttributesOnRange: function(start, end, attribs) + { + var builder = Changeset.builder(this.rep.lines.totalWidth()); + ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, start); + ChangesetUtils.buildKeepRange(this.rep, builder, start, end, attribs, this.rep.apool); + return this.applyChangeset(builder); + }, + + /* + Returns if the line already has a line marker + @param lineNum: the number of the line + */ + lineHasMarker: function(lineNum){ + var that = this; + + return _.find(lineAttributes, function(attribute){ + return that.getAttributeOnLine(lineNum, attribute) != ''; + }) !== undefined; + }, + + /* + Gets a specified attribute on a line + @param lineNum: the number of the line to set the attribute for + @param attributeKey: the name of the attribute to get, e.g. list + */ + getAttributeOnLine: function(lineNum, attributeName){ + // get `attributeName` attribute of first char of line + var aline = this.rep.alines[lineNum]; + if (aline) + { + var opIter = Changeset.opIterator(aline); + if (opIter.hasNext()) + { + return Changeset.opAttributeValue(opIter.next(), attributeName, this.rep.apool) || ''; + } + } + return ''; + }, + + /* + Sets a specified attribute on a line + @param lineNum: the number of the line to set the attribute for + @param attributeKey: the name of the attribute to set, e.g. list + @param attributeValue: an optional parameter to pass to the attribute (e.g. indention level) + + */ + setAttributeOnLine: function(lineNum, attributeName, attributeValue){ + var loc = [0,0]; + var builder = Changeset.builder(this.rep.lines.totalWidth()); + var hasMarker = this.lineHasMarker(lineNum); + + ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0])); + + if(hasMarker){ + ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 1]), [ + [attributeName, attributeValue] + ], this.rep.apool); + }else{ + // add a line marker + builder.insert('*', [ + ['author', this.author], + ['insertorder', 'first'], + [lineMarkerAttribute, '1'], + [attributeName, attributeValue] + ], this.rep.apool); + } + + return this.applyChangeset(builder); + }, + + /* + Removes a specified attribute on a line + @param lineNum: the number of the affected line + @param attributeKey: the name of the attribute to remove, e.g. list + + */ + removeAttributeOnLine: function(lineNum, attributeName, attributeValue){ + + var loc = [0,0]; + var builder = Changeset.builder(this.rep.lines.totalWidth()); + var hasMarker = this.lineHasMarker(lineNum); + + if(hasMarker){ + ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0])); + ChangesetUtils.buildRemoveRange(this.rep, builder, loc, (loc = [lineNum, 1])); + } + + return this.applyChangeset(builder); + }, + + /* + Sets a specified attribute on a line + @param lineNum: the number of the line to set the attribute for + @param attributeKey: the name of the attribute to set, e.g. list + @param attributeValue: an optional parameter to pass to the attribute (e.g. indention level) + */ + toggleAttributeOnLine: function(lineNum, attributeName, attributeValue) { + return this.getAttributeOnLine(attributeName) ? + this.removeAttributeOnLine(lineNum, attributeName) : + this.setAttributeOnLine(lineNum, attributeName, attributeValue); + + } +}); + +module.exports = AttributeManager;
\ No newline at end of file diff --git a/src/static/js/AttributePool.js b/src/static/js/AttributePool.js new file mode 100644 index 00000000..f5990c07 --- /dev/null +++ b/src/static/js/AttributePool.js @@ -0,0 +1,96 @@ +/** + * This code represents the Attribute Pool Object of the original Etherpad. + * 90% of the code is still like in the original Etherpad + * Look at https://github.com/ether/pad/blob/master/infrastructure/ace/www/easysync2.js + * You can find a explanation what a attribute pool is here: + * https://github.com/Pita/etherpad-lite/blob/master/doc/easysync/easysync-notes.txt + */ + +/* + * Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + An AttributePool maintains a mapping from [key,value] Pairs called + Attributes to Numbers (unsigened integers) and vice versa. These numbers are + used to reference Attributes in Changesets. +*/ + +var AttributePool = function () { + this.numToAttrib = {}; // e.g. {0: ['foo','bar']} + this.attribToNum = {}; // e.g. {'foo,bar': 0} + this.nextNum = 0; +}; + +AttributePool.prototype.putAttrib = function (attrib, dontAddIfAbsent) { + var str = String(attrib); + if (str in this.attribToNum) { + return this.attribToNum[str]; + } + if (dontAddIfAbsent) { + return -1; + } + var num = this.nextNum++; + this.attribToNum[str] = num; + this.numToAttrib[num] = [String(attrib[0] || ''), String(attrib[1] || '')]; + return num; +}; + +AttributePool.prototype.getAttrib = function (num) { + var pair = this.numToAttrib[num]; + if (!pair) { + return pair; + } + return [pair[0], pair[1]]; // return a mutable copy +}; + +AttributePool.prototype.getAttribKey = function (num) { + var pair = this.numToAttrib[num]; + if (!pair) return ''; + return pair[0]; +}; + +AttributePool.prototype.getAttribValue = function (num) { + var pair = this.numToAttrib[num]; + if (!pair) return ''; + return pair[1]; +}; + +AttributePool.prototype.eachAttrib = function (func) { + for (var n in this.numToAttrib) { + var pair = this.numToAttrib[n]; + func(pair[0], pair[1]); + } +}; + +AttributePool.prototype.toJsonable = function () { + return { + numToAttrib: this.numToAttrib, + nextNum: this.nextNum + }; +}; + +AttributePool.prototype.fromJsonable = function (obj) { + this.numToAttrib = obj.numToAttrib; + this.nextNum = obj.nextNum; + this.attribToNum = {}; + for (var n in this.numToAttrib) { + this.attribToNum[String(this.numToAttrib[n])] = Number(n); + } + return this; +}; + + +module.exports = AttributePool;
\ No newline at end of file diff --git a/static/js/Changeset.js b/src/static/js/Changeset.js index 81c0c81b..738ee1ba 100644 --- a/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -25,7 +25,7 @@ * limitations under the License. */ -var AttributePoolFactory = require("/AttributePoolFactory"); +var AttributePool = require("./AttributePool"); var _opt = null; @@ -1731,7 +1731,7 @@ exports.appendATextToAssembler = function (atext, assem) { * @param pool {AtributePool} */ exports.prepareForWire = function (cs, pool) { - var newPool = AttributePoolFactory.createAttributePool();; + var newPool = new AttributePool(); var newCs = exports.moveOpsToNewPool(cs, pool, newPool); return { translated: newCs, diff --git a/src/static/js/ChangesetUtils.js b/src/static/js/ChangesetUtils.js new file mode 100644 index 00000000..e0b67881 --- /dev/null +++ b/src/static/js/ChangesetUtils.js @@ -0,0 +1,60 @@ +/** + * This module contains several helper Functions to build Changesets + * based on a SkipList + */ + +/** + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +exports.buildRemoveRange = function(rep, builder, start, end) +{ + var startLineOffset = rep.lines.offsetOfIndex(start[0]); + var endLineOffset = rep.lines.offsetOfIndex(end[0]); + + if (end[0] > start[0]) + { + builder.remove(endLineOffset - startLineOffset - start[1], end[0] - start[0]); + builder.remove(end[1]); + } + else + { + builder.remove(end[1] - start[1]); + } +} + +exports.buildKeepRange = function(rep, builder, start, end, attribs, pool) +{ + var startLineOffset = rep.lines.offsetOfIndex(start[0]); + var endLineOffset = rep.lines.offsetOfIndex(end[0]); + + if (end[0] > start[0]) + { + builder.keep(endLineOffset - startLineOffset - start[1], end[0] - start[0], attribs, pool); + builder.keep(end[1], 0, attribs, pool); + } + else + { + builder.keep(end[1] - start[1], 0, attribs, pool); + } +} + +exports.buildKeepToStartOfRange = function(rep, builder, start) +{ + var startLineOffset = rep.lines.offsetOfIndex(start[0]); + + builder.keep(startLineOffset, start[0]); + builder.keep(start[1]); +} + diff --git a/static/js/ace.js b/src/static/js/ace.js index 22d4eaa6..6ea2938b 100644 --- a/static/js/ace.js +++ b/src/static/js/ace.js @@ -28,7 +28,8 @@ Ace2Editor.registry = { nextId: 1 }; -var plugins = require('/plugins').plugins; +var hooks = require('./pluginfw/hooks'); +var _ = require('./underscore'); function Ace2Editor() { @@ -70,7 +71,7 @@ function Ace2Editor() function doActionsPendingInit() { - $.each(actionsPendingInit, function(i,fn){ + _.each(actionsPendingInit, function(fn,i){ fn() }); actionsPendingInit = []; @@ -87,7 +88,7 @@ function Ace2Editor() 'setUserChangeNotificationCallback', 'setAuthorInfo', 'setAuthorSelectionRange', 'callWithAce', 'execCommand', 'replaceRange']; - $.each(aceFunctionsPendingInit, function(i,fnName){ + _.each(aceFunctionsPendingInit, function(fnName,i){ var prefix = 'ace_'; var name = prefix + fnName; editor[fnName] = pendingInit(function(){ @@ -156,7 +157,11 @@ function Ace2Editor() } function pushRequireScriptTo(buffer) { var KERNEL_SOURCE = '../static/js/require-kernel.js'; - var KERNEL_BOOT = 'require.setRootURI("../minified/");\nrequire.setGlobalKeyPath("require");' + var KERNEL_BOOT = '\ +require.setRootURI("../javascripts/src");\n\ +require.setLibraryURI("../javascripts/lib");\n\ +require.setGlobalKeyPath("require");\n\ +'; if (Ace2Editor.EMBEDED && Ace2Editor.EMBEDED[KERNEL_SOURCE]) { buffer.push('<script type="text/javascript">'); buffer.push(Ace2Editor.EMBEDED[KERNEL_SOURCE]); @@ -166,17 +171,18 @@ function Ace2Editor() } function pushScriptsTo(buffer) { /* Folling is for packaging regular expression. */ - /* $$INCLUDE_JS("../minified/ace2_inner.js?callback=require.define"); */ - var ACE_SOURCE = '../minified/ace2_inner.js?callback=require.define'; + /* $$INCLUDE_JS("../javascripts/src/ace2_inner.js?callback=require.define"); */ + var ACE_SOURCE = '../javascripts/src/ace2_inner.js?callback=require.define'; if (Ace2Editor.EMBEDED && Ace2Editor.EMBEDED[ACE_SOURCE]) { buffer.push('<script type="text/javascript">'); buffer.push(Ace2Editor.EMBEDED[ACE_SOURCE]); - buffer.push('require("/ace2_inner");'); + buffer.push('require("ep_etherpad-lite/static/js/ace2_inner");'); buffer.push('<\/script>'); } else { + file = ACE_SOURCE; buffer.push('<script type="application/javascript" src="' + ACE_SOURCE + '"><\/script>'); buffer.push('<script type="text/javascript">'); - buffer.push('require("/ace2_inner");'); + buffer.push('require("ep_etherpad-lite/static/js/ace2_inner");'); buffer.push('<\/script>'); } } @@ -227,14 +233,16 @@ function Ace2Editor() iframeHTML.push(doctype); iframeHTML.push("<html><head>"); + iframeHTML.push('<script type="text/javascript" src="../static/js/jquery.js"></script>'); + + hooks.callAll("aceInitInnerdocbodyHead", { + iframeHTML: iframeHTML + }); // For compatability's sake transform in and out. for (var i = 0, ii = iframeHTML.length; i < ii; i++) { iframeHTML[i] = JSON.stringify(iframeHTML[i]); } - plugins.callHook("aceInitInnerdocbodyHead", { - iframeHTML: iframeHTML - }); for (var i = 0, ii = iframeHTML.length; i < ii; i++) { iframeHTML[i] = JSON.parse(iframeHTML[i]); } @@ -248,6 +256,10 @@ function Ace2Editor() $$INCLUDE_CSS("../static/css/iframe_editor.css"); $$INCLUDE_CSS("../static/css/pad.css"); $$INCLUDE_CSS("../static/custom/pad.css"); + + var additionalCSS = _(hooks.callAll("aceEditorCSS")).map(function(path){ return '../static/plugins/' + path }); + includedCSS = includedCSS.concat(additionalCSS); + pushStyleTagsFor(iframeHTML, includedCSS); var includedJS = []; @@ -256,9 +268,14 @@ function Ace2Editor() // Inject my plugins into my child. iframeHTML.push('\ <script type="text/javascript">\ + parent_req = require("./pluginfw/parent_require.js");\ + parent_req.getRequirementFromParent(require, "ep_etherpad-lite/static/js/pluginfw/hooks");\ + parent_req.getRequirementFromParent(require, "ep_etherpad-lite/static/js/pluginfw/plugins");\ + parent_req.getRequirementFromParent(require, "./pluginfw/hooks");\ + parent_req.getRequirementFromParent(require, "./pluginfw/plugins");\ require.define("/plugins", null);\n\ require.define("/plugins.js", function (require, exports, module) {\ - module.exports = parent.parent.require("/plugins");\ + module.exports = require("ep_etherpad-lite/static/js/plugins");\ });\ </script>\ '); @@ -271,7 +288,7 @@ function Ace2Editor() var thisFunctionsName = "ChildAccessibleAce2Editor"; (function () {return this}())[thisFunctionsName] = Ace2Editor; - var outerScript = 'editorId = "' + info.id + '"; editorInfo = parent.' + thisFunctionsName + '.registry[editorId]; ' + 'window.onload = function() ' + '{ window.onload = null; setTimeout' + '(function() ' + '{ var iframe = document.createElement("IFRAME"); ' + 'iframe.scrolling = "no"; var outerdocbody = document.getElementById("outerdocbody"); ' + 'iframe.frameBorder = 0; iframe.allowTransparency = true; ' + // for IE + var outerScript = 'editorId = "' + info.id + '"; editorInfo = parent.' + thisFunctionsName + '.registry[editorId]; ' + 'window.onload = function() ' + '{ window.onload = null; setTimeout' + '(function() ' + '{ var iframe = document.createElement("IFRAME"); iframe.name = "ace_inner";' + 'iframe.scrolling = "no"; var outerdocbody = document.getElementById("outerdocbody"); ' + 'iframe.frameBorder = 0; iframe.allowTransparency = true; ' + // for IE 'outerdocbody.insertBefore(iframe, outerdocbody.firstChild); ' + 'iframe.ace_outerWin = window; ' + 'readyFunc = function() { editorInfo.onEditorReady(); readyFunc = null; editorInfo = null; }; ' + 'var doc = iframe.contentWindow.document; doc.open(); var text = (' + JSON.stringify(iframeHTML.join('\n')) + ');doc.write(text); doc.close(); ' + '}, 0); }'; var outerHTML = [doctype, '<html><head>'] @@ -281,6 +298,11 @@ function Ace2Editor() $$INCLUDE_CSS("../static/css/iframe_editor.css"); $$INCLUDE_CSS("../static/css/pad.css"); $$INCLUDE_CSS("../static/custom/pad.css"); + + + var additionalCSS = _(hooks.callAll("aceEditorCSS")).map(function(path){ return '../static/plugins/' + path }); + includedCSS = includedCSS.concat(additionalCSS); + pushStyleTagsFor(outerHTML, includedCSS); // bizarrely, in FF2, a file with no "external" dependencies won't finish loading properly @@ -288,6 +310,7 @@ function Ace2Editor() outerHTML.push('<link rel="stylesheet" type="text/css" href="data:text/css,"/>', '\x3cscript>\n', outerScript.replace(/<\//g, '<\\/'), '\n\x3c/script>', '</head><body id="outerdocbody"><div id="sidediv"><!-- --></div><div id="linemetricsdiv">x</div><div id="overlaysdiv"><!-- --></div></body></html>'); var outerFrame = document.createElement("IFRAME"); + outerFrame.name = "ace_outer"; outerFrame.frameBorder = 0; // for IE info.frame = outerFrame; document.getElementById(containerId).appendChild(outerFrame); diff --git a/static/js/ace2_common.js b/src/static/js/ace2_common.js index 0f8195fa..8a7d16ee 100644 --- a/static/js/ace2_common.js +++ b/src/static/js/ace2_common.js @@ -20,7 +20,7 @@ * limitations under the License. */ -var Security = require('/security'); +var Security = require('./security'); function isNodeText(node) { @@ -29,58 +29,10 @@ function isNodeText(node) function object(o) { - var f = function() - {}; + var f = function(){}; f.prototype = o; return new f(); } - -function extend(obj, props) -{ - for (var p in props) - { - obj[p] = props[p]; - } - return obj; -} - -function forEach(array, func) -{ - for (var i = 0; i < array.length; i++) - { - var result = func(array[i], i); - if (result) break; - } -} - -function map(array, func) -{ - var result = []; - // must remain compatible with "arguments" pseudo-array - for (var i = 0; i < array.length; i++) - { - if (func) result.push(func(array[i], i)); - else result.push(array[i]); - } - return result; -} - -function filter(array, func) -{ - var result = []; - // must remain compatible with "arguments" pseudo-array - for (var i = 0; i < array.length; i++) - { - if (func(array[i], i)) result.push(array[i]); - } - return result; -} - -function isArray(testObject) -{ - return testObject && typeof testObject === 'object' && !(testObject.propertyIsEnumerable('length')) && typeof testObject.length === 'number'; -} - var userAgent = (((function () {return this;})().navigator || {}).userAgent || 'node-js').toLowerCase(); // Figure out what browser is being used (stolen from jquery 1.2.1) @@ -142,21 +94,13 @@ function htmlPrettyEscape(str) } var noop = function(){}; -var identity = function(x){return x}; exports.isNodeText = isNodeText; exports.object = object; -exports.extend = extend; -exports.forEach = forEach; -exports.map = map; -exports.filter = filter; -exports.isArray = isArray; exports.browser = browser; exports.getAssoc = getAssoc; exports.setAssoc = setAssoc; exports.binarySearch = binarySearch; exports.binarySearchInfinite = binarySearchInfinite; exports.htmlPrettyEscape = htmlPrettyEscape; -exports.map = map; exports.noop = noop; -exports.identity = identity; diff --git a/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 17d02036..d2d14503 100644 --- a/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -19,39 +19,41 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -var Ace2Common = require('/ace2_common'); - -// Extract useful method defined in the other module. -var isNodeText = Ace2Common.isNodeText; -var object = Ace2Common.object; -var extend = Ace2Common.extend; -var forEach = Ace2Common.forEach; -var map = Ace2Common.map; -var filter = Ace2Common.filter; -var isArray = Ace2Common.isArray; -var browser = Ace2Common.browser; -var getAssoc = Ace2Common.getAssoc; -var setAssoc = Ace2Common.setAssoc; -var binarySearchInfinite = Ace2Common.binarySearchInfinite; -var htmlPrettyEscape = Ace2Common.htmlPrettyEscape; -var map = Ace2Common.map; -var noop = Ace2Common.noop; - -var makeChangesetTracker = require('/changesettracker').makeChangesetTracker; -var colorutils = require('/colorutils').colorutils; -var makeContentCollector = require('/contentcollector').makeContentCollector; -var makeCSSManager = require('/cssmanager').makeCSSManager; -var domline = require('/domline').domline; -var AttribPool = require('/AttributePoolFactory').createAttributePool; -var Changeset = require('/Changeset'); -var linestylefilter = require('/linestylefilter').linestylefilter; -var newSkipList = require('/skiplist').newSkipList; -var undoModule = require('/undomodule').undoModule; -var makeVirtualLineView = require('/virtual_lines').makeVirtualLineView; - +var editor, _, $, jQuery, plugins, Ace2Common; + +Ace2Common = require('./ace2_common'); + +plugins = require('ep_etherpad-lite/static/js/pluginfw/plugins'); +$ = jQuery = require('./rjquery').$; +_ = require("./underscore"); + +var isNodeText = Ace2Common.isNodeText, + browser = Ace2Common.browser, + getAssoc = Ace2Common.getAssoc, + setAssoc = Ace2Common.setAssoc, + isTextNode = Ace2Common.isTextNode, + binarySearchInfinite = Ace2Common.binarySearchInfinite, + htmlPrettyEscape = Ace2Common.htmlPrettyEscape, + noop = Ace2Common.noop; + var hooks = require('./pluginfw/hooks'); + function Ace2Inner(){ + + var makeChangesetTracker = require('./changesettracker').makeChangesetTracker; + var colorutils = require('./colorutils').colorutils; + var makeContentCollector = require('./contentcollector').makeContentCollector; + var makeCSSManager = require('./cssmanager').makeCSSManager; + var domline = require('./domline').domline; + var AttribPool = require('./AttributePool'); + var Changeset = require('./Changeset'); + var ChangesetUtils = require('./ChangesetUtils'); + var linestylefilter = require('./linestylefilter').linestylefilter; + var SkipList = require('./skiplist'); + var undoModule = require('./undomodule').undoModule; + var makeVirtualLineView = require('./virtual_lines').makeVirtualLineView; + var AttributeManager = require('./AttributeManager'); + var DEBUG = false; //$$ build script replaces the string "var DEBUG=true;//$$" with "var DEBUG=false;" // changed to false var isSetUp = false; @@ -70,7 +72,6 @@ function Ace2Inner(){ var thisAuthor = ''; var disposed = false; - var editorInfo = parent.editorInfo; var iframe = window.frameElement; @@ -81,21 +82,18 @@ function Ace2Inner(){ var overlaysdiv = lineMetricsDiv.nextSibling; initLineNumbers(); - var outsideKeyDown = function(evt) - {}; - var outsideKeyPress = function(evt) - { - return true; - }; - var outsideNotifyDirty = function() - {}; + var outsideKeyDown = noop; + + var outsideKeyPress = function(){return true;}; + + var outsideNotifyDirty = noop; // selFocusAtStart -- determines whether the selection extends "backwards", so that the focus // point (controlled with the arrow keys) is at the beginning; not supported in IE, though // native IE selections have that behavior (which we try not to interfere with). // Must be false if selection is collapsed! var rep = { - lines: newSkipList(), + lines: new SkipList(), selStart: null, selEnd: null, selFocusAtStart: false, @@ -103,6 +101,7 @@ function Ace2Inner(){ alines: [], apool: new AttribPool() }; + // lines, alltext, alines, and DOM are set up in setup() if (undoModule.enabled) { @@ -122,6 +121,7 @@ function Ace2Inner(){ iframePadRight = 0; var console = (DEBUG && window.console); + var documentAttributeManager; if (!window.console) { @@ -158,6 +158,7 @@ function Ace2Inner(){ var textFace = 'monospace'; var textSize = 12; + function textLineHeight() { @@ -685,7 +686,7 @@ function Ace2Inner(){ } else { - lines = map(text.split('\n'), textify); + lines = _.map(text.split('\n'), textify); } var newText = "\n"; if (lines.length > 0) @@ -847,7 +848,7 @@ function Ace2Inner(){ var cmdArgs = Array.prototype.slice.call(arguments, 1); if (CMDS[cmd]) { - inCallStack(cmd, function() + inCallStackIfNecessary(cmd, function() { fastIncorp(9); CMDS[cmd].apply(CMDS, cmdArgs); @@ -857,7 +858,7 @@ function Ace2Inner(){ function replaceRange(start, end, text) { - inCallStack('replaceRange', function() + inCallStackIfNecessary('replaceRange', function() { fastIncorp(9); performDocumentReplaceRange(start, end, text); @@ -884,9 +885,7 @@ function Ace2Inner(){ { return fn(editorInfo); }; - - - + if (normalize !== undefined) { var wrapper1 = wrapper; @@ -934,7 +933,10 @@ function Ace2Inner(){ }, grayedout: setClassPresenceNamed(outerWin.document.body, "grayedout"), dmesg: function(){ dmesg = window.dmesg = value; }, - userauthor: function(value){ thisAuthor = String(value); }, + userauthor: function(value){ + thisAuthor = String(value); + documentAttributeManager.author = thisAuthor; + }, styled: setStyled, textface: setTextFace, textsize: setTextSize, @@ -1160,7 +1162,7 @@ function Ace2Inner(){ return; } - inCallStack("idleWorkTimer", function() + inCallStackIfNecessary("idleWorkTimer", function() { var isTimeUp = newTimeLimit(250); @@ -1623,8 +1625,7 @@ function Ace2Inner(){ } //var fragment = magicdom.wrapDom(document.createDocumentFragment()); domInsertsNeeded.push([nodeToAddAfter, lineNodeInfos]); - forEach(dirtyNodes, function(n) - { + _.each(dirtyNodes,function(n){ toDeleteAtEnd.push(n); }); var spliceHints = {}; @@ -1646,7 +1647,7 @@ function Ace2Inner(){ // update the representation p.mark("splice"); - forEach(splicesToDo, function(splice) + _.each(splicesToDo, function(splice) { doIncorpLineSplice(splice[0], splice[1], splice[2], splice[3], splice[4]); }); @@ -1656,14 +1657,14 @@ function Ace2Inner(){ //var isTimeUp = newTimeLimit(100); // do DOM inserts p.mark("insert"); - forEach(domInsertsNeeded, function(ins) + _.each(domInsertsNeeded,function(ins) { insertDomLines(ins[0], ins[1], isTimeUp); }); p.mark("del"); // delete old dom nodes - forEach(toDeleteAtEnd, function(n) + _.each(toDeleteAtEnd,function(n) { //var id = n.uniqueId(); // parent of n may not be "root" in IE due to non-tree-shaped DOM (wtf) @@ -1773,7 +1774,7 @@ function Ace2Inner(){ var charEnd = rep.lines.offsetOfEntry(endEntry) + endEntry.width; //rep.lexer.lexCharRange([charStart, charEnd], isTimeUp); - forEach(infoStructs, function(info) + _.each(infoStructs, function(info) { var p2 = PROFILER("insertLine", false); var node = info.node; @@ -1870,55 +1871,6 @@ function Ace2Inner(){ } } - - function setupMozillaCaretHack(lineNum) - { - // This is really ugly, but by god, it works! - // Fixes annoying Firefox caret artifact (observed in 2.0.0.12 - // and unfixed in Firefox 2 as of now) where mutating the DOM - // and then moving the caret to the beginning of a line causes - // an image of the caret to be XORed at the top of the iframe. - // The previous solution involved remembering to set the selection - // later, in response to the next event in the queue, which was hugely - // annoying. - // This solution: add a space character (0x20) to the beginning of the line. - // After setting the selection, remove the space. - var lineNode = rep.lines.atIndex(lineNum).lineNode; - - var fc = lineNode.firstChild; - while (isBlockElement(fc) && fc.firstChild) - { - fc = fc.firstChild; - } - var textNode; - if (isNodeText(fc)) - { - fc.nodeValue = " " + fc.nodeValue; - textNode = fc; - } - else - { - textNode = doc.createTextNode(" "); - fc.parentNode.insertBefore(textNode, fc); - } - markNodeClean(lineNode); - return { - unhack: function() - { - if (textNode.nodeValue == " ") - { - textNode.parentNode.removeChild(textNode); - } - else - { - textNode.nodeValue = textNode.nodeValue.substring(1); - } - markNodeClean(lineNode); - } - }; - } - - function getPointForLineAndChar(lineAndChar) { var line = lineAndChar[0]; @@ -2049,6 +2001,7 @@ function Ace2Inner(){ return [lineNum, col]; } } + editorInfo.ace_getLineAndCharForPoint = getLineAndCharForPoint; function createDomLineEntry(lineString) { @@ -2084,10 +2037,8 @@ function Ace2Inner(){ var linesMutatee = { splice: function(start, numRemoved, newLinesVA) { - domAndRepSplice(start, numRemoved, map(Array.prototype.slice.call(arguments, 2), function(s) - { - return s.slice(0, -1); - }), null); + var args = Array.prototype.slice.call(arguments, 2); + domAndRepSplice(start, numRemoved, _.map(args, function(s){ return s.slice(0, -1); }), null); }, get: function(i) { @@ -2099,7 +2050,7 @@ function Ace2Inner(){ }, slice_notused: function(start, end) { - return map(rep.lines.slice(start, end), function(e) + return _.map(rep.lines.slice(start, end), function(e) { return e.text + '\n'; }); @@ -2132,7 +2083,7 @@ function Ace2Inner(){ } } - var lineEntries = map(newLineStrings, createDomLineEntry); + var lineEntries = _.map(newLineStrings, createDomLineEntry); doRepLineSplice(startLine, deleteCount, lineEntries); @@ -2143,12 +2094,12 @@ function Ace2Inner(){ } else nodeToAddAfter = null; - insertDomLines(nodeToAddAfter, map(lineEntries, function(entry) + insertDomLines(nodeToAddAfter, _.map(lineEntries, function(entry) { return entry.domInfo; }), isTimeUp); - forEach(keysToDelete, function(k) + _.each(keysToDelete, function(k) { var n = doc.getElementById(k); n.parentNode.removeChild(n); @@ -2254,6 +2205,9 @@ function Ace2Inner(){ } + /* + Converts the position of a char (index in String) into a [row, col] tuple + */ function lineAndColumnFromChar(x) { var lineEntry = rep.lines.atOffset(x); @@ -2308,8 +2262,8 @@ function Ace2Inner(){ // CCCC\n // end[0]: <CCC end[1] CCC>-------\n var builder = Changeset.builder(rep.lines.totalWidth()); - buildKeepToStartOfRange(builder, start); - buildRemoveRange(builder, start, end); + ChangesetUtils.buildKeepToStartOfRange(rep, builder, start); + ChangesetUtils.buildRemoveRange(rep, builder, start, end); builder.insert(newText, [ ['author', thisAuthor] ], rep.apool); @@ -2320,68 +2274,17 @@ function Ace2Inner(){ function performDocumentApplyAttributesToCharRange(start, end, attribs) { - if (end >= rep.alltext.length) - { - end = rep.alltext.length - 1; - } - performDocumentApplyAttributesToRange(lineAndColumnFromChar(start), lineAndColumnFromChar(end), attribs); + end = Math.min(end, rep.alltext.length - 1); + documentAttributeManager.setAttributesOnRange(lineAndColumnFromChar(start), lineAndColumnFromChar(end), attribs); } editorInfo.ace_performDocumentApplyAttributesToCharRange = performDocumentApplyAttributesToCharRange; - - function performDocumentApplyAttributesToRange(start, end, attribs) - { - var builder = Changeset.builder(rep.lines.totalWidth()); - buildKeepToStartOfRange(builder, start); - buildKeepRange(builder, start, end, attribs, rep.apool); - var cs = builder.toString(); - performDocumentApplyChangeset(cs); - } - - function buildKeepToStartOfRange(builder, start) - { - var startLineOffset = rep.lines.offsetOfIndex(start[0]); - - builder.keep(startLineOffset, start[0]); - builder.keep(start[1]); - } - - function buildRemoveRange(builder, start, end) - { - var startLineOffset = rep.lines.offsetOfIndex(start[0]); - var endLineOffset = rep.lines.offsetOfIndex(end[0]); - - if (end[0] > start[0]) - { - builder.remove(endLineOffset - startLineOffset - start[1], end[0] - start[0]); - builder.remove(end[1]); - } - else - { - builder.remove(end[1] - start[1]); - } - } - - function buildKeepRange(builder, start, end, attribs, pool) - { - var startLineOffset = rep.lines.offsetOfIndex(start[0]); - var endLineOffset = rep.lines.offsetOfIndex(end[0]); - - if (end[0] > start[0]) - { - builder.keep(endLineOffset - startLineOffset - start[1], end[0] - start[0], attribs, pool); - builder.keep(end[1], 0, attribs, pool); - } - else - { - builder.keep(end[1] - start[1], 0, attribs, pool); - } - } - + + function setAttributeOnSelection(attributeName, attributeValue) { if (!(rep.selStart && rep.selEnd)) return; - performDocumentApplyAttributesToRange(rep.selStart, rep.selEnd, [ + documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [ [attributeName, attributeValue] ]); } @@ -2442,13 +2345,13 @@ function Ace2Inner(){ if (selectionAllHasIt) { - performDocumentApplyAttributesToRange(rep.selStart, rep.selEnd, [ + documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [ [attributeName, ''] ]); } else { - performDocumentApplyAttributesToRange(rep.selStart, rep.selEnd, [ + documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [ [attributeName, 'true'] ]); } @@ -2468,7 +2371,7 @@ function Ace2Inner(){ function doRepLineSplice(startLine, deleteCount, newLineEntries) { - forEach(newLineEntries, function(entry) + _.each(newLineEntries, function(entry) { entry.width = entry.text.length + 1; }); @@ -2483,7 +2386,7 @@ function Ace2Inner(){ currentCallStack.repChanged = true; var newRegionEnd = rep.lines.offsetOfIndex(startLine + newLineEntries.length); - var newText = map(newLineEntries, function(e) + var newText = _.map(newLineEntries, function(e) { return e.text + '\n'; }).join(''); @@ -2513,7 +2416,7 @@ function Ace2Inner(){ selEndHintChar = rep.lines.offsetOfIndex(hints.selEnd[0]) + hints.selEnd[1] - oldRegionStart; } - var newText = map(newLineEntries, function(e) + var newText = _.map(newLineEntries, function(e) { return e.text + '\n'; }).join(''); @@ -2861,6 +2764,7 @@ function Ace2Inner(){ currentCallStack.selectionAffected = true; } } + editorInfo.ace_performSelectionChange = performSelectionChange; // Change the abstract representation of the document to have a different selection. // Should not rely on the line representation. Should not affect the DOM. @@ -2969,6 +2873,10 @@ function Ace2Inner(){ "ul": 1 }; + _.each(hooks.callAll('aceRegisterBlockElements'), function(element){ + _blockElems[element] = 1; + }); + function isBlockElement(n) { return !!_blockElems[(n.tagName || "").toLowerCase()]; @@ -3054,7 +2962,7 @@ function Ace2Inner(){ { // returns index of cleanRange containing i, or -1 if none var answer = -1; - forEach(cleanRanges, function(r, idx) + _.each(cleanRanges ,function(r, idx) { if (i >= r[1]) return false; // keep looking if (i < r[0]) return true; // not found, stop looking @@ -3288,7 +3196,7 @@ function Ace2Inner(){ function handleClick(evt) { - inCallStack("handleClick", function() + inCallStackIfNecessary("handleClick", function() { idleWorkTimer.atMost(200); }); @@ -3333,6 +3241,7 @@ function Ace2Inner(){ { return; } + var lineNum = rep.selStart[0]; var listType = getLineListType(lineNum); @@ -3404,11 +3313,9 @@ function Ace2Inner(){ } } - if (mods.length > 0) - { - setLineListTypes(mods); - } - + _.each(mods, function(mod){ + setLineListType(mod[0], mod[1]); + }); return true; } editorInfo.ace_doIndentOutdent = doIndentOutdent; @@ -3458,6 +3365,9 @@ function Ace2Inner(){ var thisLineListType = getLineListType(theLine); var prevLineEntry = (theLine > 0 && rep.lines.atIndex(theLine - 1)); var prevLineBlank = (prevLineEntry && prevLineEntry.text.length == prevLineEntry.lineMarker); + + var thisLineHasMarker = documentAttributeManager.lineHasMarker(theLine); + if (thisLineListType) { // this line is a list @@ -3471,6 +3381,9 @@ function Ace2Inner(){ // delistify performDocumentReplaceRange([theLine, 0], [theLine, lineEntry.lineMarker], ''); } + }else if (thisLineHasMarker && prevLineEntry){ + // If the line has any attributes assigned, remove them by removing the marker '*' + performDocumentReplaceRange([theLine -1 , prevLineEntry.text.length], [theLine, lineEntry.lineMarker], ''); } else if (theLine > 0) { @@ -3610,7 +3523,7 @@ function Ace2Inner(){ var stopped = false; - inCallStack("handleKeyEvent", function() + inCallStackIfNecessary("handleKeyEvent", function() { if (type == "keypress" || (isTypeForSpecialKey && keyCode == 13 /*return*/ )) @@ -3823,31 +3736,22 @@ function Ace2Inner(){ return; } - var mozillaCaretHack = (false && browser.mozilla && selStart && selEnd && selStart[0] == selEnd[0] && selStart[1] == rep.lines.atIndex(selStart[0]).lineMarker && selEnd[1] == rep.lines.atIndex(selEnd[0]).lineMarker && setupMozillaCaretHack(selStart[0])); - var selection = {}; var ss = [selStart[0], selStart[1]]; - if (mozillaCaretHack) ss[1] += 1; selection.startPoint = getPointForLineAndChar(ss); var se = [selEnd[0], selEnd[1]]; - if (mozillaCaretHack) se[1] += 1; selection.endPoint = getPointForLineAndChar(se); selection.focusAtStart = !! rep.selFocusAtStart; setSelection(selection); - - if (mozillaCaretHack) - { - mozillaCaretHack.unhack(); - } } function getRepHTML() { - return map(rep.lines.slice(), function(entry) + return _.map(rep.lines.slice(), function(entry) { var text = entry.text; var content; @@ -4533,7 +4437,7 @@ function Ace2Inner(){ enforceEditability(); - addClass(sideDiv, 'sidedivdelayed'); + $(sideDiv).addClass('sidedivdelayed'); } function getScrollXY() @@ -4586,14 +4490,12 @@ function Ace2Inner(){ function teardown() { - forEach(_teardownActions, function(a) + _.each(_teardownActions, function(a) { a(); }); } - bindEventHandler(window, "load", setup); - function setDesignMode(newVal) { try @@ -4670,20 +4572,20 @@ function Ace2Inner(){ function bindTheEventHandlers() { - bindEventHandler(document, "keydown", handleKeyEvent); - bindEventHandler(document, "keypress", handleKeyEvent); - bindEventHandler(document, "keyup", handleKeyEvent); - bindEventHandler(document, "click", handleClick); - bindEventHandler(root, "blur", handleBlur); + $(document).on("keydown", handleKeyEvent); + $(document).on("keypress", handleKeyEvent); + $(document).on("keyup", handleKeyEvent); + $(document).on("click", handleClick); + $(root).on("blur", handleBlur); if (browser.msie) { - bindEventHandler(document, "click", handleIEOuterClick); + $(document).on("click", handleIEOuterClick); } - if (browser.msie) bindEventHandler(root, "paste", handleIEPaste); + if (browser.msie) $(root).on("paste", handleIEPaste); if ((!browser.msie) && document.documentElement) { - bindEventHandler(document.documentElement, "compositionstart", handleCompositionEvent); - bindEventHandler(document.documentElement, "compositionend", handleCompositionEvent); + $(document.documentElement).on("compositionstart", handleCompositionEvent); + $(document.documentElement).on("compositionend", handleCompositionEvent); } } @@ -4699,7 +4601,7 @@ function Ace2Inner(){ } // click below the body - inCallStack("handleOuterClick", function() + inCallStackIfNecessary("handleOuterClick", function() { // put caret at bottom of doc fastIncorp(11); @@ -4730,49 +4632,16 @@ function Ace2Inner(){ elem.className = array.join(' '); } - function addClass(elem, className) - { - var seen = false; - var cc = getClassArray(elem, function(c) - { - if (c == className) seen = true; - return true; - }); - if (!seen) - { - cc.push(className); - setClassArray(elem, cc); - } - } - - function removeClass(elem, className) - { - var seen = false; - var cc = getClassArray(elem, function(c) - { - if (c == className) - { - seen = true; - return false; - } - return true; - }); - if (seen) - { - setClassArray(elem, cc); - } - } - function setClassPresence(elem, className, present) { - if (present) addClass(elem, className); - else removeClass(elem, className); + if (present) $(elem).addClass(className); + else $(elem).removeClass(elem, className); } function setup() { doc = document; // defined as a var in scope outside - inCallStack("setup", function() + inCallStackIfNecessary("setup", function() { var body = doc.getElementById("innerdocbody"); root = body; // defined as a var in scope outside @@ -4833,32 +4702,6 @@ function Ace2Inner(){ } } - function bindEventHandler(target, type, func) - { - var handler; - if ((typeof func._wrapper) != "function") - { - func._wrapper = function(event) - { - func(fixEvent(event || window.event || {})); - } - } - var handler = func._wrapper; - if (target.addEventListener) target.addEventListener(type, handler, false); - else target.attachEvent("on" + type, handler); - _teardownActions.push(function() - { - unbindEventHandler(target, type, func); - }); - } - - function unbindEventHandler(target, type, func) - { - var handler = func._wrapper; - if (target.removeEventListener) target.removeEventListener(type, handler, false); - else target.detachEvent("on" + type, handler); - } - function getSelectionPointX(point) { // doesn't work in wrap-mode @@ -4992,27 +4835,30 @@ function Ace2Inner(){ } } } - + + var listAttributeName = 'list'; + function getLineListType(lineNum) { - // get "list" attribute of first char of line - var aline = rep.alines[lineNum]; - if (aline) - { - var opIter = Changeset.opIterator(aline); - if (opIter.hasNext()) - { - return Changeset.opAttributeValue(opIter.next(), 'list', rep.apool) || ''; - } - } - return ''; + return documentAttributeManager.getAttributeOnLine(lineNum, listAttributeName) } function setLineListType(lineNum, listType) { - setLineListTypes([ - [lineNum, listType] - ]); + if(listType == ''){ + documentAttributeManager.removeAttributeOnLine(lineNum, listAttributeName); + }else{ + documentAttributeManager.setAttributeOnLine(lineNum, listAttributeName, listType); + } + + //if the list has been removed, it is necessary to renumber + //starting from the *next* line because the list may have been + //separated. If it returns null, it means that the list was not cut, try + //from the current one. + if(renumberList(lineNum+1)==null) + { + renumberList(lineNum); + } } function renumberList(lineNum){ @@ -5059,8 +4905,8 @@ function Ace2Inner(){ } else if(curLevel == level) { - buildKeepRange(builder, loc, (loc = [line, 0])); - buildKeepRange(builder, loc, (loc = [line, 1]), [ + ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 0])); + ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 1]), [ ['start', position] ], rep.apool); @@ -5091,62 +4937,6 @@ function Ace2Inner(){ } - function setLineListTypes(lineNumTypePairsInOrder) - { - var loc = [0, 0]; - var builder = Changeset.builder(rep.lines.totalWidth()); - for (var i = 0; i < lineNumTypePairsInOrder.length; i++) - { - var pair = lineNumTypePairsInOrder[i]; - var lineNum = pair[0]; - var listType = pair[1]; - buildKeepRange(builder, loc, (loc = [lineNum, 0])); - if (getLineListType(lineNum)) - { - // already a line marker - if (listType) - { - // make different list type - buildKeepRange(builder, loc, (loc = [lineNum, 1]), [ - ['list', listType] - ], rep.apool); - } - else - { - // remove list marker - buildRemoveRange(builder, loc, (loc = [lineNum, 1])); - } - } - else - { - // currently no line marker - if (listType) - { - // add a line marker - builder.insert('*', [ - ['author', thisAuthor], - ['insertorder', 'first'], - ['list', listType] - ], rep.apool); - } - } - } - - var cs = builder.toString(); - if (!Changeset.isIdentity(cs)) - { - performDocumentApplyChangeset(cs); - } - - //if the list has been removed, it is necessary to renumber - //starting from the *next* line because the list may have been - //separated. If it returns null, it means that the list was not cut, try - //from the current one. - if(renumberList(lineNum+1)==null) - { - renumberList(lineNum); - } - } function doInsertList(type) { @@ -5184,7 +4974,10 @@ function Ace2Inner(){ var t = getLineListType(n); mods.push([n, allLinesAreList ? 'indent' + level : (t ? type + level : type + '1')]); } - setLineListTypes(mods); + + _.each(mods, function(mod){ + setLineListType(mod[0], mod[1]); + }); } function doInsertUnorderedList(){ @@ -5520,67 +5313,7 @@ function Ace2Inner(){ } }; })()); - - - // stolen from jquery-1.2.1 - - - function fixEvent(event) - { - // store a copy of the original event object - // and clone to set read-only properties - var originalEvent = event; - event = extend( - {}, originalEvent); - - // add preventDefault and stopPropagation since - // they will not work on the clone - event.preventDefault = function() - { - // if preventDefault exists run it on the original event - if (originalEvent.preventDefault) originalEvent.preventDefault(); - // otherwise set the returnValue property of the original event to false (IE) - originalEvent.returnValue = false; - }; - event.stopPropagation = function() - { - // if stopPropagation exists run it on the original event - if (originalEvent.stopPropagation) originalEvent.stopPropagation(); - // otherwise set the cancelBubble property of the original event to true (IE) - originalEvent.cancelBubble = true; - }; - - // Fix target property, if necessary - if (!event.target && event.srcElement) event.target = event.srcElement; - - // check if target is a textnode (safari) - if (browser.safari && event.target.nodeType == 3) event.target = originalEvent.target.parentNode; - - // Add relatedTarget, if necessary - if (!event.relatedTarget && event.fromElement) event.relatedTarget = event.fromElement == event.target ? event.toElement : event.fromElement; - - // Calculate pageX/Y if missing and clientX/Y available - if (event.pageX == null && event.clientX != null) - { - var e = document.documentElement, - b = document.body; - event.pageX = event.clientX + (e && e.scrollLeft || b.scrollLeft || 0); - event.pageY = event.clientY + (e && e.scrollTop || b.scrollTop || 0); - } - - // Add which for key events - if (!event.which && (event.charCode || event.keyCode)) event.which = event.charCode || event.keyCode; - - // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs) - if (!event.metaKey && event.ctrlKey) event.metaKey = event.ctrlKey; - - // Add which for click: 1 == left; 2 == middle; 3 == right - // Note: button is not normalized, so don't use it - if (!event.which && event.button) event.which = (event.button & 1 ? 1 : (event.button & 2 ? 3 : (event.button & 4 ? 2 : 0))); - - return event; - } - + var lineNumbersShown; var sideDivInner; @@ -5664,7 +5397,68 @@ function Ace2Inner(){ } } } + + + // Init documentAttributeManager + documentAttributeManager = new AttributeManager(rep, performDocumentApplyChangeset); + editorInfo.ace_performDocumentApplyAttributesToRange = documentAttributeManager.setAttributesOnRange; + + $(document).ready(function(){ + doc = document; // defined as a var in scope outside + inCallStack("setup", function() + { + var body = doc.getElementById("innerdocbody"); + root = body; // defined as a var in scope outside + if (browser.mozilla) $(root).addClass("mozilla"); + if (browser.safari) $(root).addClass("safari"); + if (browser.msie) $(root).addClass("msie"); + if (browser.msie) + { + // cache CSS background images + try + { + doc.execCommand("BackgroundImageCache", false, true); + } + catch (e) + { /* throws an error in some IE 6 but not others! */ + } + } + setClassPresence(root, "authorColors", true); + setClassPresence(root, "doesWrap", doesWrap); + + initDynamicCSS(); + + enforceEditability(); + + // set up dom and rep + while (root.firstChild) root.removeChild(root.firstChild); + var oneEntry = createDomLineEntry(""); + doRepLineSplice(0, rep.lines.length(), [oneEntry]); + insertDomLines(null, [oneEntry.domInfo], null); + rep.alines = Changeset.splitAttributionLines( + Changeset.makeAttribution("\n"), "\n"); + + bindTheEventHandlers(); + + }); + + hooks.callAll('aceInitialized', { + editorInfo: editorInfo, + rep: rep, + documentAttributeManager: documentAttributeManager + }); + + scheduler.setTimeout(function() + { + parent.readyFunc(); // defined in code that sets up the inner iframe + }, 0); + + isSetUp = true; + }); } -exports.editor = new Ace2Inner(); +// Ensure that plugins are loaded before initializing the editor +plugins.ensure(function () { + var editor = new Ace2Inner(); +}); diff --git a/static/js/broadcast.js b/src/static/js/broadcast.js index 97256bbf..1c7bdcfd 100644 --- a/static/js/broadcast.js +++ b/src/static/js/broadcast.js @@ -20,16 +20,13 @@ * limitations under the License. */ -var makeCSSManager = require('/cssmanager').makeCSSManager; -var domline = require('/domline').domline; -var AttribPool = require('/AttributePoolFactory').createAttributePool; -var Changeset = require('/Changeset'); -var linestylefilter = require('/linestylefilter').linestylefilter; -var colorutils = require('/colorutils').colorutils; -var Ace2Common = require('./ace2_common'); - -var map = Ace2Common.map; -var forEach = Ace2Common.forEach; +var makeCSSManager = require('./cssmanager').makeCSSManager; +var domline = require('./domline').domline; +var AttribPool = require('./AttributePool'); +var Changeset = require('./Changeset'); +var linestylefilter = require('./linestylefilter').linestylefilter; +var colorutils = require('./colorutils').colorutils; +var _ = require('./underscore'); // These parameters were global, now they are injected. A reference to the // Timeslider controller would probably be more appropriate. @@ -155,7 +152,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro // splice the lines splice: function(start, numRemoved, newLinesVA) { - var newLines = map(Array.prototype.slice.call(arguments, 2), function(s) { + var newLines = _.map(Array.prototype.slice.call(arguments, 2), function(s) { return s; }); @@ -278,7 +275,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro debugLog('Time Delta: ', timeDelta) updateTimer(); - var authors = map(padContents.getActiveAuthors(), function(name) + var authors = _.map(padContents.getActiveAuthors(), function(name) { return authorData[name]; }); @@ -384,7 +381,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro changesetLoader.queueUp(start, 1, update); } - var authors = map(padContents.getActiveAuthors(), function(name){ + var authors = _.map(padContents.getActiveAuthors(), function(name){ return authorData[name]; }); BroadcastSlider.setAuthors(authors); @@ -527,7 +524,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro authorMap[obj.author] = obj.data; receiveAuthorData(authorMap); - var authors = map(padContents.getActiveAuthors(),function(name) { + var authors = _.map(padContents.getActiveAuthors(), function(name) { return authorData[name]; }); @@ -607,10 +604,13 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro setChannelState("DISCONNECTED", reason); } + /// Since its not used, import 'forEach' has been dropped /*window['onloadFuncts'] = []; window.onload = function () { window['isloaded'] = true; + + forEach(window['onloadFuncts'],function (funct) { funct(); diff --git a/static/js/broadcast_revisions.js b/src/static/js/broadcast_revisions.js index 19f3f5ff..19f3f5ff 100644 --- a/static/js/broadcast_revisions.js +++ b/src/static/js/broadcast_revisions.js diff --git a/static/js/broadcast_slider.js b/src/static/js/broadcast_slider.js index 2b67ac73..a2a15773 100644 --- a/static/js/broadcast_slider.js +++ b/src/static/js/broadcast_slider.js @@ -22,7 +22,7 @@ // These parameters were global, now they are injected. A reference to the // Timeslider controller would probably be more appropriate. -var forEach = require('./ace2_common').forEach; +var _ = require('./underscore'); function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) { @@ -170,41 +170,67 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) $('#error').show(); } + var fixPadHeight = _.throttle(function(){ + var height = $('#timeslider-top').height(); + $('#editorcontainerbox').css({marginTop: height}); + }, 600); + function setAuthors(authors) { - $("#authorstable").empty(); + var authorsList = $("#authorsList"); + authorsList.empty(); var numAnonymous = 0; var numNamed = 0; - forEach(authors, function(author) + var colorsAnonymous = []; + _.each(authors, function(author) { + var authorColor = clientVars.colorPalette[author.colorId] || author.colorId; if (author.name) { + if (numNamed !== 0) authorsList.append(', '); + + $('<span />') + .text(author.name || "unnamed") + .css('background-color', authorColor) + .addClass('author') + .appendTo(authorsList); + numNamed++; - var tr = $('<tr></tr>'); - var swatchtd = $('<td></td>'); - var swatch = $('<div class="swatch"></div>'); - swatch.css('background-color', clientVars.colorPalette[author.colorId]); - swatchtd.append(swatch); - tr.append(swatchtd); - var nametd = $('<td></td>'); - nametd.text(author.name || "unnamed"); - tr.append(nametd); - $("#authorstable").append(tr); } else { numAnonymous++; + if(authorColor) colorsAnonymous.push(authorColor); } }); if (numAnonymous > 0) { - var html = "<tr><td colspan=\"2\" style=\"color:#999; padding-left: 10px\">" + (numNamed > 0 ? "...and " : "") + numAnonymous + " unnamed author" + (numAnonymous > 1 ? "s" : "") + "</td></tr>"; - $("#authorstable").append($(html)); + var anonymousAuthorString = numAnonymous + " unnamed author" + (numAnonymous > 1 ? "s" : "") + if (numNamed !== 0){ + authorsList.append(' + ' + anonymousAuthorString); + } else { + authorsList.append(anonymousAuthorString); + } + + if(colorsAnonymous.length > 0){ + authorsList.append(' ('); + _.each(colorsAnonymous, function(color, i){ + if( i > 0 ) authorsList.append(' '); + $('<span /> ') + .css('background-color', color) + .addClass('author author-anonymous') + .appendTo(authorsList); + }); + authorsList.append(')'); + } + } if (authors.length == 0) { - $("#authorstable").append($("<tr><td colspan=\"2\" style=\"color:#999; padding-left: 10px\">No Authors</td></tr>")) + authorsList.append("No Authors"); } + + fixPadHeight(); } BroadcastSlider = { @@ -465,11 +491,10 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) { if (clientVars.supportsSlider) { - $("#padmain, #rightbars").css('top', "130px"); $("#timeslider").show(); setSliderLength(clientVars.totalRevs); setSliderPosition(clientVars.revNum); - forEach(clientVars.savedRevisions, function(revision) + _.each(clientVars.savedRevisions, function(revision) { addSavedRevision(revision.revNum, revision); }) diff --git a/static/js/changesettracker.js b/src/static/js/changesettracker.js index e34dc107..58ef21cb 100644 --- a/static/js/changesettracker.js +++ b/src/static/js/changesettracker.js @@ -20,8 +20,8 @@ * limitations under the License. */ -var AttribPool = require('/AttributePoolFactory').createAttributePool; -var Changeset = require('/Changeset'); +var AttributePool = require('./AttributePool'); +var Changeset = require('./Changeset'); function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) { @@ -83,7 +83,7 @@ function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) baseAText = Changeset.cloneAText(atext); if (apoolJsonObj) { - var wireApool = (new AttribPool()).fromJsonable(apoolJsonObj); + var wireApool = (new AttributePool()).fromJsonable(apoolJsonObj); baseAText.attribs = Changeset.moveOpsToNewPool(baseAText.attribs, wireApool, apool); } submittedChangeset = null; @@ -117,7 +117,7 @@ function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) if (apoolJsonObj) { - var wireApool = (new AttribPool()).fromJsonable(apoolJsonObj); + var wireApool = (new AttributePool()).fromJsonable(apoolJsonObj); c = Changeset.moveOpsToNewPool(c, wireApool, apool); } diff --git a/static/js/chat.js b/src/static/js/chat.js index 8f076af6..23b47667 100644 --- a/static/js/chat.js +++ b/src/static/js/chat.js @@ -20,8 +20,8 @@ * limitations under the License. */ -var padutils = require('/pad_utils').padutils; -var padcookie = require('/pad_cookie').padcookie; +var padutils = require('./pad_utils').padutils; +var padcookie = require('./pad_cookie').padcookie; var chat = (function() { @@ -147,10 +147,11 @@ var chat = (function() } }); - for(var i in clientVars.chatHistory) - { - this.addMessage(clientVars.chatHistory[i], false); - } + var that = this; + $.each(clientVars.chatHistory, function(i, o){ + that.addMessage(o, false); + }) + $("#chatcounter").text(clientVars.chatHistory.length); } } diff --git a/static/js/collab_client.js b/src/static/js/collab_client.js index bb68f604..18e3616b 100644 --- a/static/js/collab_client.js +++ b/src/static/js/collab_client.js @@ -20,7 +20,7 @@ * limitations under the License. */ -var chat = require('/chat').chat; +var chat = require('./chat').chat; // Dependency fill on init. This exists for `pad.socket` only. // TODO: bind directly to the socket. diff --git a/static/js/colorutils.js b/src/static/js/colorutils.js index 5fbefb4d..5fbefb4d 100644 --- a/static/js/colorutils.js +++ b/src/static/js/colorutils.js diff --git a/static/js/contentcollector.js b/src/static/js/contentcollector.js index 96dc4b7d..219976d3 100644 --- a/static/js/contentcollector.js +++ b/src/static/js/contentcollector.js @@ -25,8 +25,9 @@ var _MAX_LIST_LEVEL = 8; -var Changeset = require('/Changeset'); -var plugins = require('/plugins').plugins; +var Changeset = require('./Changeset'); +var hooks = require('./pluginfw/hooks'); +var _ = require('./underscore'); function sanitizeUnicode(s) { @@ -37,8 +38,6 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class { browser = browser || {}; - var plugins_ = plugins; - var dom = domInterface || { isNodeText: function(n) { @@ -163,12 +162,6 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class var selection, startPoint, endPoint; var selStart = [-1, -1], selEnd = [-1, -1]; - var blockElems = { - "div": 1, - "p": 1, - "pre": 1 - }; - function _isEmpty(node, state) { // consider clean blank lines pasted in IE to be empty @@ -190,7 +183,7 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class { var ln = lines.length() - 1; var chr = lines.textOfLine(ln).length; - if (chr == 0 && state.listType && state.listType != 'none') + if (chr == 0 && !_.isEmpty(state.lineAttributes)) { chr += 1; // listMarker } @@ -242,25 +235,30 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class function _enterList(state, listType) { - var oldListType = state.listType; - state.listLevel = (state.listLevel || 0) + 1; + var oldListType = state.lineAttributes['list']; if (listType != 'none') { state.listNesting = (state.listNesting || 0) + 1; } - state.listType = listType; + + if(listType === 'none' || !listType ){ + delete state.lineAttributes['list']; + } + else{ + state.lineAttributes['list'] = listType; + } + _recalcAttribString(state); return oldListType; } function _exitList(state, oldListType) { - state.listLevel--; - if (state.listType != 'none') + if (state.lineAttributes['list']) { state.listNesting--; } - state.listType = oldListType; + if(oldListType) state.lineAttributes['list'] = oldListType; _recalcAttribString(state); } @@ -303,21 +301,29 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class state.attribString = Changeset.makeAttribsString('+', lst, apool); } - function _produceListMarker(state) + function _produceLineAttributesMarker(state) { - lines.appendText('*', Changeset.makeAttribsString('+', [ - ['list', state.listType], + // TODO: This has to go to AttributeManager. + var attributes = [ + ['lmkr', '1'], ['insertorder', 'first'] - ], apool)); + ].concat( + _.map(state.lineAttributes,function(value,key){ + console.log([key, value]) + return [key, value]; + }) + ); + + lines.appendText('*', Changeset.makeAttribsString('+', attributes , apool)); } cc.startNewLine = function(state) { if (state) { var atBeginningOfLine = lines.textOfLine(lines.length() - 1).length == 0; - if (atBeginningOfLine && state.listType && state.listType != 'none') + if (atBeginningOfLine && !_.isEmpty(state.lineAttributes)) { - _produceListMarker(state); + _produceLineAttributesMarker(state); } } lines.startNew(); @@ -347,7 +353,14 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class localAttribs: null, attribs: { /*name -> nesting counter*/ }, - attribString: '' + attribString: '', + // lineAttributes maintain a map from attributes to attribute values set on a line + lineAttributes: { + /* + example: + 'list': 'bullet1', + */ + } }; } var localAttribs = state.localAttribs; @@ -409,9 +422,9 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class // newlines in the source mustn't become spaces at beginning of line box txt2 = txt2.replace(/^\n*/, ''); } - if (atBeginningOfLine && state.listType && state.listType != 'none') + if (atBeginningOfLine && !_.isEmpty(state.lineAttributes)) { - _produceListMarker(state); + _produceLineAttributesMarker(state); } lines.appendText(textify(txt2), state.attribString); x += consumed; @@ -448,7 +461,7 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class var oldAuthorOrNull = null; if (collectStyles) { - plugins_.callHook('collectContentPre', { + hooks.callAll('collectContentPre', { cc: cc, state: state, tname: tname, @@ -510,7 +523,7 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class if (collectStyles) { - plugins_.callHook('collectContentPost', { + hooks.callAll('collectContentPost', { cc: cc, state: state, tname: tname, diff --git a/static/js/cssmanager.js b/src/static/js/cssmanager.js index 46075e57..46075e57 100644 --- a/static/js/cssmanager.js +++ b/src/static/js/cssmanager.js diff --git a/static/js/domline.js b/src/static/js/domline.js index 3074c9e9..5d8bb719 100644 --- a/static/js/domline.js +++ b/src/static/js/domline.js @@ -26,12 +26,13 @@ // requires: plugins // requires: undefined -var Security = require('/security'); -var Ace2Common = require('/ace2_common'); -var plugins = require('/plugins').plugins; -var map = Ace2Common.map; +var Security = require('./security'); +var hooks = require('./pluginfw/hooks'); +var _ = require('./underscore'); +var lineAttributeMarker = require('./linestylefilter').lineAttributeMarker; +var Ace2Common = require('./ace2_common'); var noop = Ace2Common.noop; -var identity = Ace2Common.identity; + var domline = {}; @@ -81,7 +82,8 @@ domline.createDomLine = function(nonEmpty, doesWrap, optBrowser, optDocument) } var html = []; - var preHtml, postHtml; + var preHtml = '', + postHtml = ''; var curHTML = null; function processSpaces(s) @@ -89,12 +91,14 @@ domline.createDomLine = function(nonEmpty, doesWrap, optBrowser, optDocument) return domline.processSpaces(s, doesWrap); } - var perTextNodeProcess = (doesWrap ? identity : processSpaces); - var perHtmlLineProcess = (doesWrap ? processSpaces : identity); + var perTextNodeProcess = (doesWrap ? _.identity : processSpaces); + var perHtmlLineProcess = (doesWrap ? processSpaces : _.identity); var lineClass = 'ace-line'; result.appendSpan = function(txt, cls) { - if (cls.indexOf('list') >= 0) + var processedMarker = false; + // Handle lineAttributeMarker, if present + if (cls.indexOf(lineAttributeMarker) >= 0) { var listType = /(?:^| )list:(\S+)/.exec(cls); var start = /(?:^| )start:(\S+)/.exec(cls); @@ -114,10 +118,26 @@ domline.createDomLine = function(nonEmpty, doesWrap, optBrowser, optDocument) preHtml = '<ol '+start+' class="list-' + Security.escapeHTMLAttribute(listType) + '"><li>'; postHtml = '</li></ol>'; } - } + } + processedMarker = true; + } + + _.map(hooks.callAll("aceDomLineProcessLineAttributes", { + domline: domline, + cls: cls + }), function(modifier) + { + preHtml += modifier.preHtml; + postHtml += modifier.postHtml; + processedMarker |= modifier.processedMarker; + }); + + if( processedMarker ){ result.lineMarker += txt.length; return; // don't append any text - } + } + + } var href = null; var simpleTags = null; @@ -142,9 +162,7 @@ domline.createDomLine = function(nonEmpty, doesWrap, optBrowser, optDocument) var extraOpenTags = ""; var extraCloseTags = ""; - var plugins_ = plugins; - - map(plugins_.callHook("aceCreateDomLine", { + _.map(hooks.callAll("aceCreateDomLine", { domline: domline, cls: cls }), function(modifier) @@ -204,7 +222,7 @@ domline.createDomLine = function(nonEmpty, doesWrap, optBrowser, optDocument) { newHTML = (preHtml || '') + newHTML + (postHtml || ''); } - html = preHtml = postHtml = null; // free memory + html = preHtml = postHtml = ''; // free memory if (newHTML !== curHTML) { curHTML = newHTML; diff --git a/static/js/draggable.js b/src/static/js/draggable.js index 8d197545..8d197545 100644 --- a/static/js/draggable.js +++ b/src/static/js/draggable.js diff --git a/static/js/excanvas.js b/src/static/js/excanvas.js index a34ca1da..a34ca1da 100644 --- a/static/js/excanvas.js +++ b/src/static/js/excanvas.js diff --git a/static/js/farbtastic.js b/src/static/js/farbtastic.js index 0045703d..0045703d 100644 --- a/static/js/farbtastic.js +++ b/src/static/js/farbtastic.js diff --git a/src/static/js/jquery.js b/src/static/js/jquery.js new file mode 100644 index 00000000..8ccd0ea7 --- /dev/null +++ b/src/static/js/jquery.js @@ -0,0 +1,9266 @@ +/*! + * jQuery JavaScript Library v1.7.1 + * http://jquery.com/ + * + * Copyright 2011, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * Copyright 2011, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * + * Date: Mon Nov 21 21:11:03 2011 -0500 + */ +(function( window, undefined ) { + +// Use the correct document accordingly with window argument (sandbox) +var document = window.document, + navigator = window.navigator, + location = window.location; +var jQuery = (function() { + +// Define a local copy of jQuery +var jQuery = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + return new jQuery.fn.init( selector, context, rootjQuery ); + }, + + // Map over jQuery in case of overwrite + _jQuery = window.jQuery, + + // Map over the $ in case of overwrite + _$ = window.$, + + // A central reference to the root jQuery(document) + rootjQuery, + + // A simple way to check for HTML strings or ID strings + // Prioritize #id over <tag> to avoid XSS via location.hash (#9521) + quickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/, + + // Check if a string has a non-whitespace character in it + rnotwhite = /\S/, + + // Used for trimming whitespace + trimLeft = /^\s+/, + trimRight = /\s+$/, + + // Match a standalone tag + rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/, + + // JSON RegExp + rvalidchars = /^[\],:{}\s]*$/, + rvalidescape = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, + rvalidtokens = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, + rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, + + // Useragent RegExp + rwebkit = /(webkit)[ \/]([\w.]+)/, + ropera = /(opera)(?:.*version)?[ \/]([\w.]+)/, + rmsie = /(msie) ([\w.]+)/, + rmozilla = /(mozilla)(?:.*? rv:([\w.]+))?/, + + // Matches dashed string for camelizing + rdashAlpha = /-([a-z]|[0-9])/ig, + rmsPrefix = /^-ms-/, + + // Used by jQuery.camelCase as callback to replace() + fcamelCase = function( all, letter ) { + return ( letter + "" ).toUpperCase(); + }, + + // Keep a UserAgent string for use with jQuery.browser + userAgent = navigator.userAgent, + + // For matching the engine and version of the browser + browserMatch, + + // The deferred used on DOM ready + readyList, + + // The ready event handler + DOMContentLoaded, + + // Save a reference to some core methods + toString = Object.prototype.toString, + hasOwn = Object.prototype.hasOwnProperty, + push = Array.prototype.push, + slice = Array.prototype.slice, + trim = String.prototype.trim, + indexOf = Array.prototype.indexOf, + + // [[Class]] -> type pairs + class2type = {}; + +jQuery.fn = jQuery.prototype = { + constructor: jQuery, + init: function( selector, context, rootjQuery ) { + var match, elem, ret, doc; + + // Handle $(""), $(null), or $(undefined) + if ( !selector ) { + return this; + } + + // Handle $(DOMElement) + if ( selector.nodeType ) { + this.context = this[0] = selector; + this.length = 1; + return this; + } + + // The body element only exists once, optimize finding it + if ( selector === "body" && !context && document.body ) { + this.context = document; + this[0] = document.body; + this.selector = selector; + this.length = 1; + return this; + } + + // Handle HTML strings + if ( typeof selector === "string" ) { + // Are we dealing with HTML string or an ID? + if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = quickExpr.exec( selector ); + } + + // Verify a match, and that no context was specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) { + context = context instanceof jQuery ? context[0] : context; + doc = ( context ? context.ownerDocument || context : document ); + + // If a single string is passed in and it's a single tag + // just do a createElement and skip the rest + ret = rsingleTag.exec( selector ); + + if ( ret ) { + if ( jQuery.isPlainObject( context ) ) { + selector = [ document.createElement( ret[1] ) ]; + jQuery.fn.attr.call( selector, context, true ); + + } else { + selector = [ doc.createElement( ret[1] ) ]; + } + + } else { + ret = jQuery.buildFragment( [ match[1] ], [ doc ] ); + selector = ( ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment ).childNodes; + } + + return jQuery.merge( this, selector ); + + // HANDLE: $("#id") + } else { + elem = document.getElementById( match[2] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id !== match[2] ) { + return rootjQuery.find( selector ); + } + + // Otherwise, we inject the element directly into the jQuery object + this.length = 1; + this[0] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || rootjQuery ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) { + return rootjQuery.ready( selector ); + } + + if ( selector.selector !== undefined ) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray( selector, this ); + }, + + // Start with an empty selector + selector: "", + + // The current version of jQuery being used + jquery: "1.7.1", + + // The default length of a jQuery object is 0 + length: 0, + + // The number of elements contained in the matched element set + size: function() { + return this.length; + }, + + toArray: function() { + return slice.call( this, 0 ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num == null ? + + // Return a 'clean' array + this.toArray() : + + // Return just the object + ( num < 0 ? this[ this.length + num ] : this[ num ] ); + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems, name, selector ) { + // Build a new jQuery matched element set + var ret = this.constructor(); + + if ( jQuery.isArray( elems ) ) { + push.apply( ret, elems ); + + } else { + jQuery.merge( ret, elems ); + } + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + ret.context = this.context; + + if ( name === "find" ) { + ret.selector = this.selector + ( this.selector ? " " : "" ) + selector; + } else if ( name ) { + ret.selector = this.selector + "." + name + "(" + selector + ")"; + } + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function( callback, args ) { + return jQuery.each( this, callback, args ); + }, + + ready: function( fn ) { + // Attach the listeners + jQuery.bindReady(); + + // Add the callback + readyList.add( fn ); + + return this; + }, + + eq: function( i ) { + i = +i; + return i === -1 ? + this.slice( i ) : + this.slice( i, i + 1 ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ), + "slice", slice.call(arguments).join(",") ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map(this, function( elem, i ) { + return callback.call( elem, i, elem ); + })); + }, + + end: function() { + return this.prevObject || this.constructor(null); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: [].sort, + splice: [].splice +}; + +// Give the init function the jQuery prototype for later instantiation +jQuery.fn.init.prototype = jQuery.fn; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[0] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction(target) ) { + target = {}; + } + + // extend jQuery itself if only one argument is passed + if ( length === i ) { + target = this; + --i; + } + + for ( ; i < length; i++ ) { + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) { + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { + if ( copyIsArray ) { + copyIsArray = false; + clone = src && jQuery.isArray(src) ? src : []; + + } else { + clone = src && jQuery.isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend({ + noConflict: function( deep ) { + if ( window.$ === jQuery ) { + window.$ = _$; + } + + if ( deep && window.jQuery === jQuery ) { + window.jQuery = _jQuery; + } + + return jQuery; + }, + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Hold (or release) the ready event + holdReady: function( hold ) { + if ( hold ) { + jQuery.readyWait++; + } else { + jQuery.ready( true ); + } + }, + + // Handle when the DOM is ready + ready: function( wait ) { + // Either a released hold or an DOMready/load event and not yet ready + if ( (wait === true && !--jQuery.readyWait) || (wait !== true && !jQuery.isReady) ) { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( !document.body ) { + return setTimeout( jQuery.ready, 1 ); + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.fireWith( document, [ jQuery ] ); + + // Trigger any bound ready events + if ( jQuery.fn.trigger ) { + jQuery( document ).trigger( "ready" ).off( "ready" ); + } + } + }, + + bindReady: function() { + if ( readyList ) { + return; + } + + readyList = jQuery.Callbacks( "once memory" ); + + // Catch cases where $(document).ready() is called after the + // browser event has already occurred. + if ( document.readyState === "complete" ) { + // Handle it asynchronously to allow scripts the opportunity to delay ready + return setTimeout( jQuery.ready, 1 ); + } + + // Mozilla, Opera and webkit nightlies currently support this event + if ( document.addEventListener ) { + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", jQuery.ready, false ); + + // If IE event model is used + } else if ( document.attachEvent ) { + // ensure firing before onload, + // maybe late but safe also for iframes + document.attachEvent( "onreadystatechange", DOMContentLoaded ); + + // A fallback to window.onload, that will always work + window.attachEvent( "onload", jQuery.ready ); + + // If IE and not a frame + // continually check to see if the document is ready + var toplevel = false; + + try { + toplevel = window.frameElement == null; + } catch(e) {} + + if ( document.documentElement.doScroll && toplevel ) { + doScrollCheck(); + } + } + }, + + // See test/unit/core.js for details concerning isFunction. + // Since version 1.3, DOM methods and functions like alert + // aren't supported. They return false on IE (#2968). + isFunction: function( obj ) { + return jQuery.type(obj) === "function"; + }, + + isArray: Array.isArray || function( obj ) { + return jQuery.type(obj) === "array"; + }, + + // A crude way of determining if an object is a window + isWindow: function( obj ) { + return obj && typeof obj === "object" && "setInterval" in obj; + }, + + isNumeric: function( obj ) { + return !isNaN( parseFloat(obj) ) && isFinite( obj ); + }, + + type: function( obj ) { + return obj == null ? + String( obj ) : + class2type[ toString.call(obj) ] || "object"; + }, + + isPlainObject: function( obj ) { + // Must be an Object. + // Because of IE, we also have to check the presence of the constructor property. + // Make sure that DOM nodes and window objects don't pass through, as well + if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { + return false; + } + + try { + // Not own constructor property must be Object + if ( obj.constructor && + !hasOwn.call(obj, "constructor") && + !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { + return false; + } + } catch ( e ) { + // IE8,9 Will throw exceptions on certain host objects #9897 + return false; + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + + var key; + for ( key in obj ) {} + + return key === undefined || hasOwn.call( obj, key ); + }, + + isEmptyObject: function( obj ) { + for ( var name in obj ) { + return false; + } + return true; + }, + + error: function( msg ) { + throw new Error( msg ); + }, + + parseJSON: function( data ) { + if ( typeof data !== "string" || !data ) { + return null; + } + + // Make sure leading/trailing whitespace is removed (IE can't handle it) + data = jQuery.trim( data ); + + // Attempt to parse using the native JSON parser first + if ( window.JSON && window.JSON.parse ) { + return window.JSON.parse( data ); + } + + // Make sure the incoming data is actual JSON + // Logic borrowed from http://json.org/json2.js + if ( rvalidchars.test( data.replace( rvalidescape, "@" ) + .replace( rvalidtokens, "]" ) + .replace( rvalidbraces, "")) ) { + + return ( new Function( "return " + data ) )(); + + } + jQuery.error( "Invalid JSON: " + data ); + }, + + // Cross-browser xml parsing + parseXML: function( data ) { + var xml, tmp; + try { + if ( window.DOMParser ) { // Standard + tmp = new DOMParser(); + xml = tmp.parseFromString( data , "text/xml" ); + } else { // IE + xml = new ActiveXObject( "Microsoft.XMLDOM" ); + xml.async = "false"; + xml.loadXML( data ); + } + } catch( e ) { + xml = undefined; + } + if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) { + jQuery.error( "Invalid XML: " + data ); + } + return xml; + }, + + noop: function() {}, + + // Evaluates a script in a global context + // Workarounds based on findings by Jim Driscoll + // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context + globalEval: function( data ) { + if ( data && rnotwhite.test( data ) ) { + // We use execScript on Internet Explorer + // We use an anonymous function so that context is window + // rather than jQuery in Firefox + ( window.execScript || function( data ) { + window[ "eval" ].call( window, data ); + } )( data ); + } + }, + + // Convert dashed to camelCase; used by the css and data modules + // Microsoft forgot to hump their vendor prefix (#9572) + camelCase: function( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase(); + }, + + // args is for internal usage only + each: function( object, callback, args ) { + var name, i = 0, + length = object.length, + isObj = length === undefined || jQuery.isFunction( object ); + + if ( args ) { + if ( isObj ) { + for ( name in object ) { + if ( callback.apply( object[ name ], args ) === false ) { + break; + } + } + } else { + for ( ; i < length; ) { + if ( callback.apply( object[ i++ ], args ) === false ) { + break; + } + } + } + + // A special, fast, case for the most common use of each + } else { + if ( isObj ) { + for ( name in object ) { + if ( callback.call( object[ name ], name, object[ name ] ) === false ) { + break; + } + } + } else { + for ( ; i < length; ) { + if ( callback.call( object[ i ], i, object[ i++ ] ) === false ) { + break; + } + } + } + } + + return object; + }, + + // Use native String.trim function wherever possible + trim: trim ? + function( text ) { + return text == null ? + "" : + trim.call( text ); + } : + + // Otherwise use our own trimming functionality + function( text ) { + return text == null ? + "" : + text.toString().replace( trimLeft, "" ).replace( trimRight, "" ); + }, + + // results is for internal usage only + makeArray: function( array, results ) { + var ret = results || []; + + if ( array != null ) { + // The window, strings (and functions) also have 'length' + // Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930 + var type = jQuery.type( array ); + + if ( array.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( array ) ) { + push.call( ret, array ); + } else { + jQuery.merge( ret, array ); + } + } + + return ret; + }, + + inArray: function( elem, array, i ) { + var len; + + if ( array ) { + if ( indexOf ) { + return indexOf.call( array, elem, i ); + } + + len = array.length; + i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; + + for ( ; i < len; i++ ) { + // Skip accessing in sparse arrays + if ( i in array && array[ i ] === elem ) { + return i; + } + } + } + + return -1; + }, + + merge: function( first, second ) { + var i = first.length, + j = 0; + + if ( typeof second.length === "number" ) { + for ( var l = second.length; j < l; j++ ) { + first[ i++ ] = second[ j ]; + } + + } else { + while ( second[j] !== undefined ) { + first[ i++ ] = second[ j++ ]; + } + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, inv ) { + var ret = [], retVal; + inv = !!inv; + + // Go through the array, only saving the items + // that pass the validator function + for ( var i = 0, length = elems.length; i < length; i++ ) { + retVal = !!callback( elems[ i ], i ); + if ( inv !== retVal ) { + ret.push( elems[ i ] ); + } + } + + return ret; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var value, key, ret = [], + i = 0, + length = elems.length, + // jquery objects are treated as arrays + isArray = elems instanceof jQuery || length !== undefined && typeof length === "number" && ( ( length > 0 && elems[ 0 ] && elems[ length -1 ] ) || length === 0 || jQuery.isArray( elems ) ) ; + + // Go through the array, translating each of the items to their + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + + // Go through every key on the object, + } else { + for ( key in elems ) { + value = callback( elems[ key ], key, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + } + + // Flatten any nested arrays + return ret.concat.apply( [], ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // Bind a function to a context, optionally partially applying any + // arguments. + proxy: function( fn, context ) { + if ( typeof context === "string" ) { + var tmp = fn[ context ]; + context = fn; + fn = tmp; + } + + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if ( !jQuery.isFunction( fn ) ) { + return undefined; + } + + // Simulated bind + var args = slice.call( arguments, 2 ), + proxy = function() { + return fn.apply( context, args.concat( slice.call( arguments ) ) ); + }; + + // Set the guid of unique handler to the same of original handler, so it can be removed + proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++; + + return proxy; + }, + + // Mutifunctional method to get and set values to a collection + // The value/s can optionally be executed if it's a function + access: function( elems, key, value, exec, fn, pass ) { + var length = elems.length; + + // Setting many attributes + if ( typeof key === "object" ) { + for ( var k in key ) { + jQuery.access( elems, k, key[k], exec, fn, value ); + } + return elems; + } + + // Setting one attribute + if ( value !== undefined ) { + // Optionally, function values get executed if exec is true + exec = !pass && exec && jQuery.isFunction(value); + + for ( var i = 0; i < length; i++ ) { + fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass ); + } + + return elems; + } + + // Getting an attribute + return length ? fn( elems[0], key ) : undefined; + }, + + now: function() { + return ( new Date() ).getTime(); + }, + + // Use of jQuery.browser is frowned upon. + // More details: http://docs.jquery.com/Utilities/jQuery.browser + uaMatch: function( ua ) { + ua = ua.toLowerCase(); + + var match = rwebkit.exec( ua ) || + ropera.exec( ua ) || + rmsie.exec( ua ) || + ua.indexOf("compatible") < 0 && rmozilla.exec( ua ) || + []; + + return { browser: match[1] || "", version: match[2] || "0" }; + }, + + sub: function() { + function jQuerySub( selector, context ) { + return new jQuerySub.fn.init( selector, context ); + } + jQuery.extend( true, jQuerySub, this ); + jQuerySub.superclass = this; + jQuerySub.fn = jQuerySub.prototype = this(); + jQuerySub.fn.constructor = jQuerySub; + jQuerySub.sub = this.sub; + jQuerySub.fn.init = function init( selector, context ) { + if ( context && context instanceof jQuery && !(context instanceof jQuerySub) ) { + context = jQuerySub( context ); + } + + return jQuery.fn.init.call( this, selector, context, rootjQuerySub ); + }; + jQuerySub.fn.init.prototype = jQuerySub.fn; + var rootjQuerySub = jQuerySub(document); + return jQuerySub; + }, + + browser: {} +}); + +// Populate the class2type map +jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +}); + +browserMatch = jQuery.uaMatch( userAgent ); +if ( browserMatch.browser ) { + jQuery.browser[ browserMatch.browser ] = true; + jQuery.browser.version = browserMatch.version; +} + +// Deprecated, use jQuery.browser.webkit instead +if ( jQuery.browser.webkit ) { + jQuery.browser.safari = true; +} + +// IE doesn't match non-breaking spaces with \s +if ( rnotwhite.test( "\xA0" ) ) { + trimLeft = /^[\s\xA0]+/; + trimRight = /[\s\xA0]+$/; +} + +// All jQuery objects should point back to these +rootjQuery = jQuery(document); + +// Cleanup functions for the document ready method +if ( document.addEventListener ) { + DOMContentLoaded = function() { + document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + jQuery.ready(); + }; + +} else if ( document.attachEvent ) { + DOMContentLoaded = function() { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( document.readyState === "complete" ) { + document.detachEvent( "onreadystatechange", DOMContentLoaded ); + jQuery.ready(); + } + }; +} + +// The DOM ready check for Internet Explorer +function doScrollCheck() { + if ( jQuery.isReady ) { + return; + } + + try { + // If IE is used, use the trick by Diego Perini + // http://javascript.nwbox.com/IEContentLoaded/ + document.documentElement.doScroll("left"); + } catch(e) { + setTimeout( doScrollCheck, 1 ); + return; + } + + // and execute any waiting functions + jQuery.ready(); +} + +return jQuery; + +})(); + + +// String to Object flags format cache +var flagsCache = {}; + +// Convert String-formatted flags into Object-formatted ones and store in cache +function createFlags( flags ) { + var object = flagsCache[ flags ] = {}, + i, length; + flags = flags.split( /\s+/ ); + for ( i = 0, length = flags.length; i < length; i++ ) { + object[ flags[i] ] = true; + } + return object; +} + +/* + * Create a callback list using the following parameters: + * + * flags: an optional list of space-separated flags that will change how + * the callback list behaves + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible flags: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( flags ) { + + // Convert flags from String-formatted to Object-formatted + // (we check in cache first) + flags = flags ? ( flagsCache[ flags ] || createFlags( flags ) ) : {}; + + var // Actual callback list + list = [], + // Stack of fire calls for repeatable lists + stack = [], + // Last fire value (for non-forgettable lists) + memory, + // Flag to know if list is currently firing + firing, + // First callback to fire (used internally by add and fireWith) + firingStart, + // End of the loop when firing + firingLength, + // Index of currently firing callback (modified by remove if needed) + firingIndex, + // Add one or several callbacks to the list + add = function( args ) { + var i, + length, + elem, + type, + actual; + for ( i = 0, length = args.length; i < length; i++ ) { + elem = args[ i ]; + type = jQuery.type( elem ); + if ( type === "array" ) { + // Inspect recursively + add( elem ); + } else if ( type === "function" ) { + // Add if not in unique mode and callback is not in + if ( !flags.unique || !self.has( elem ) ) { + list.push( elem ); + } + } + } + }, + // Fire callbacks + fire = function( context, args ) { + args = args || []; + memory = !flags.memory || [ context, args ]; + firing = true; + firingIndex = firingStart || 0; + firingStart = 0; + firingLength = list.length; + for ( ; list && firingIndex < firingLength; firingIndex++ ) { + if ( list[ firingIndex ].apply( context, args ) === false && flags.stopOnFalse ) { + memory = true; // Mark as halted + break; + } + } + firing = false; + if ( list ) { + if ( !flags.once ) { + if ( stack && stack.length ) { + memory = stack.shift(); + self.fireWith( memory[ 0 ], memory[ 1 ] ); + } + } else if ( memory === true ) { + self.disable(); + } else { + list = []; + } + } + }, + // Actual Callbacks object + self = { + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + var length = list.length; + add( arguments ); + // Do we need to add the callbacks to the + // current firing batch? + if ( firing ) { + firingLength = list.length; + // With memory, if we're not firing then + // we should call right away, unless previous + // firing was halted (stopOnFalse) + } else if ( memory && memory !== true ) { + firingStart = length; + fire( memory[ 0 ], memory[ 1 ] ); + } + } + return this; + }, + // Remove a callback from the list + remove: function() { + if ( list ) { + var args = arguments, + argIndex = 0, + argLength = args.length; + for ( ; argIndex < argLength ; argIndex++ ) { + for ( var i = 0; i < list.length; i++ ) { + if ( args[ argIndex ] === list[ i ] ) { + // Handle firingIndex and firingLength + if ( firing ) { + if ( i <= firingLength ) { + firingLength--; + if ( i <= firingIndex ) { + firingIndex--; + } + } + } + // Remove the element + list.splice( i--, 1 ); + // If we have some unicity property then + // we only need to do this once + if ( flags.unique ) { + break; + } + } + } + } + } + return this; + }, + // Control if a given callback is in the list + has: function( fn ) { + if ( list ) { + var i = 0, + length = list.length; + for ( ; i < length; i++ ) { + if ( fn === list[ i ] ) { + return true; + } + } + } + return false; + }, + // Remove all callbacks from the list + empty: function() { + list = []; + return this; + }, + // Have the list do nothing anymore + disable: function() { + list = stack = memory = undefined; + return this; + }, + // Is it disabled? + disabled: function() { + return !list; + }, + // Lock the list in its current state + lock: function() { + stack = undefined; + if ( !memory || memory === true ) { + self.disable(); + } + return this; + }, + // Is it locked? + locked: function() { + return !stack; + }, + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( stack ) { + if ( firing ) { + if ( !flags.once ) { + stack.push( [ context, args ] ); + } + } else if ( !( flags.once && memory ) ) { + fire( context, args ); + } + } + return this; + }, + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + // To know if the callbacks have already been called at least once + fired: function() { + return !!memory; + } + }; + + return self; +}; + + + + +var // Static reference to slice + sliceDeferred = [].slice; + +jQuery.extend({ + + Deferred: function( func ) { + var doneList = jQuery.Callbacks( "once memory" ), + failList = jQuery.Callbacks( "once memory" ), + progressList = jQuery.Callbacks( "memory" ), + state = "pending", + lists = { + resolve: doneList, + reject: failList, + notify: progressList + }, + promise = { + done: doneList.add, + fail: failList.add, + progress: progressList.add, + + state: function() { + return state; + }, + + // Deprecated + isResolved: doneList.fired, + isRejected: failList.fired, + + then: function( doneCallbacks, failCallbacks, progressCallbacks ) { + deferred.done( doneCallbacks ).fail( failCallbacks ).progress( progressCallbacks ); + return this; + }, + always: function() { + deferred.done.apply( deferred, arguments ).fail.apply( deferred, arguments ); + return this; + }, + pipe: function( fnDone, fnFail, fnProgress ) { + return jQuery.Deferred(function( newDefer ) { + jQuery.each( { + done: [ fnDone, "resolve" ], + fail: [ fnFail, "reject" ], + progress: [ fnProgress, "notify" ] + }, function( handler, data ) { + var fn = data[ 0 ], + action = data[ 1 ], + returned; + if ( jQuery.isFunction( fn ) ) { + deferred[ handler ](function() { + returned = fn.apply( this, arguments ); + if ( returned && jQuery.isFunction( returned.promise ) ) { + returned.promise().then( newDefer.resolve, newDefer.reject, newDefer.notify ); + } else { + newDefer[ action + "With" ]( this === deferred ? newDefer : this, [ returned ] ); + } + }); + } else { + deferred[ handler ]( newDefer[ action ] ); + } + }); + }).promise(); + }, + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + if ( obj == null ) { + obj = promise; + } else { + for ( var key in promise ) { + obj[ key ] = promise[ key ]; + } + } + return obj; + } + }, + deferred = promise.promise({}), + key; + + for ( key in lists ) { + deferred[ key ] = lists[ key ].fire; + deferred[ key + "With" ] = lists[ key ].fireWith; + } + + // Handle state + deferred.done( function() { + state = "resolved"; + }, failList.disable, progressList.lock ).fail( function() { + state = "rejected"; + }, doneList.disable, progressList.lock ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( firstParam ) { + var args = sliceDeferred.call( arguments, 0 ), + i = 0, + length = args.length, + pValues = new Array( length ), + count = length, + pCount = length, + deferred = length <= 1 && firstParam && jQuery.isFunction( firstParam.promise ) ? + firstParam : + jQuery.Deferred(), + promise = deferred.promise(); + function resolveFunc( i ) { + return function( value ) { + args[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value; + if ( !( --count ) ) { + deferred.resolveWith( deferred, args ); + } + }; + } + function progressFunc( i ) { + return function( value ) { + pValues[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value; + deferred.notifyWith( promise, pValues ); + }; + } + if ( length > 1 ) { + for ( ; i < length; i++ ) { + if ( args[ i ] && args[ i ].promise && jQuery.isFunction( args[ i ].promise ) ) { + args[ i ].promise().then( resolveFunc(i), deferred.reject, progressFunc(i) ); + } else { + --count; + } + } + if ( !count ) { + deferred.resolveWith( deferred, args ); + } + } else if ( deferred !== firstParam ) { + deferred.resolveWith( deferred, length ? [ firstParam ] : [] ); + } + return promise; + } +}); + + + + +jQuery.support = (function() { + + var support, + all, + a, + select, + opt, + input, + marginDiv, + fragment, + tds, + events, + eventName, + i, + isSupported, + div = document.createElement( "div" ), + documentElement = document.documentElement; + + // Preliminary tests + div.setAttribute("className", "t"); + div.innerHTML = " <link/><table></table><a href='/a' style='top:1px;float:left;opacity:.55;'>a</a><input type='checkbox'/>"; + + all = div.getElementsByTagName( "*" ); + a = div.getElementsByTagName( "a" )[ 0 ]; + + // Can't get basic test support + if ( !all || !all.length || !a ) { + return {}; + } + + // First batch of supports tests + select = document.createElement( "select" ); + opt = select.appendChild( document.createElement("option") ); + input = div.getElementsByTagName( "input" )[ 0 ]; + + support = { + // IE strips leading whitespace when .innerHTML is used + leadingWhitespace: ( div.firstChild.nodeType === 3 ), + + // Make sure that tbody elements aren't automatically inserted + // IE will insert them into empty tables + tbody: !div.getElementsByTagName("tbody").length, + + // Make sure that link elements get serialized correctly by innerHTML + // This requires a wrapper element in IE + htmlSerialize: !!div.getElementsByTagName("link").length, + + // Get the style information from getAttribute + // (IE uses .cssText instead) + style: /top/.test( a.getAttribute("style") ), + + // Make sure that URLs aren't manipulated + // (IE normalizes it by default) + hrefNormalized: ( a.getAttribute("href") === "/a" ), + + // Make sure that element opacity exists + // (IE uses filter instead) + // Use a regex to work around a WebKit issue. See #5145 + opacity: /^0.55/.test( a.style.opacity ), + + // Verify style float existence + // (IE uses styleFloat instead of cssFloat) + cssFloat: !!a.style.cssFloat, + + // Make sure that if no value is specified for a checkbox + // that it defaults to "on". + // (WebKit defaults to "" instead) + checkOn: ( input.value === "on" ), + + // Make sure that a selected-by-default option has a working selected property. + // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) + optSelected: opt.selected, + + // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) + getSetAttribute: div.className !== "t", + + // Tests for enctype support on a form(#6743) + enctype: !!document.createElement("form").enctype, + + // Makes sure cloning an html5 element does not cause problems + // Where outerHTML is undefined, this still works + html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav></:nav>", + + // Will be defined later + submitBubbles: true, + changeBubbles: true, + focusinBubbles: false, + deleteExpando: true, + noCloneEvent: true, + inlineBlockNeedsLayout: false, + shrinkWrapBlocks: false, + reliableMarginRight: true + }; + + // Make sure checked status is properly cloned + input.checked = true; + support.noCloneChecked = input.cloneNode( true ).checked; + + // Make sure that the options inside disabled selects aren't marked as disabled + // (WebKit marks them as disabled) + select.disabled = true; + support.optDisabled = !opt.disabled; + + // Test to see if it's possible to delete an expando from an element + // Fails in Internet Explorer + try { + delete div.test; + } catch( e ) { + support.deleteExpando = false; + } + + if ( !div.addEventListener && div.attachEvent && div.fireEvent ) { + div.attachEvent( "onclick", function() { + // Cloning a node shouldn't copy over any + // bound event handlers (IE does this) + support.noCloneEvent = false; + }); + div.cloneNode( true ).fireEvent( "onclick" ); + } + + // Check if a radio maintains its value + // after being appended to the DOM + input = document.createElement("input"); + input.value = "t"; + input.setAttribute("type", "radio"); + support.radioValue = input.value === "t"; + + input.setAttribute("checked", "checked"); + div.appendChild( input ); + fragment = document.createDocumentFragment(); + fragment.appendChild( div.lastChild ); + + // WebKit doesn't clone checked state correctly in fragments + support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Check if a disconnected checkbox will retain its checked + // value of true after appended to the DOM (IE6/7) + support.appendChecked = input.checked; + + fragment.removeChild( input ); + fragment.appendChild( div ); + + div.innerHTML = ""; + + // Check if div with explicit width and no margin-right incorrectly + // gets computed margin-right based on width of container. For more + // info see bug #3333 + // Fails in WebKit before Feb 2011 nightlies + // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right + if ( window.getComputedStyle ) { + marginDiv = document.createElement( "div" ); + marginDiv.style.width = "0"; + marginDiv.style.marginRight = "0"; + div.style.width = "2px"; + div.appendChild( marginDiv ); + support.reliableMarginRight = + ( parseInt( ( window.getComputedStyle( marginDiv, null ) || { marginRight: 0 } ).marginRight, 10 ) || 0 ) === 0; + } + + // Technique from Juriy Zaytsev + // http://perfectionkills.com/detecting-event-support-without-browser-sniffing/ + // We only care about the case where non-standard event systems + // are used, namely in IE. Short-circuiting here helps us to + // avoid an eval call (in setAttribute) which can cause CSP + // to go haywire. See: https://developer.mozilla.org/en/Security/CSP + if ( div.attachEvent ) { + for( i in { + submit: 1, + change: 1, + focusin: 1 + }) { + eventName = "on" + i; + isSupported = ( eventName in div ); + if ( !isSupported ) { + div.setAttribute( eventName, "return;" ); + isSupported = ( typeof div[ eventName ] === "function" ); + } + support[ i + "Bubbles" ] = isSupported; + } + } + + fragment.removeChild( div ); + + // Null elements to avoid leaks in IE + fragment = select = opt = marginDiv = div = input = null; + + // Run tests that need a body at doc ready + jQuery(function() { + var container, outer, inner, table, td, offsetSupport, + conMarginTop, ptlm, vb, style, html, + body = document.getElementsByTagName("body")[0]; + + if ( !body ) { + // Return for frameset docs that don't have a body + return; + } + + conMarginTop = 1; + ptlm = "position:absolute;top:0;left:0;width:1px;height:1px;margin:0;"; + vb = "visibility:hidden;border:0;"; + style = "style='" + ptlm + "border:5px solid #000;padding:0;'"; + html = "<div " + style + "><div></div></div>" + + "<table " + style + " cellpadding='0' cellspacing='0'>" + + "<tr><td></td></tr></table>"; + + container = document.createElement("div"); + container.style.cssText = vb + "width:0;height:0;position:static;top:0;margin-top:" + conMarginTop + "px"; + body.insertBefore( container, body.firstChild ); + + // Construct the test element + div = document.createElement("div"); + container.appendChild( div ); + + // Check if table cells still have offsetWidth/Height when they are set + // to display:none and there are still other visible table cells in a + // table row; if so, offsetWidth/Height are not reliable for use when + // determining if an element has been hidden directly using + // display:none (it is still safe to use offsets if a parent element is + // hidden; don safety goggles and see bug #4512 for more information). + // (only IE 8 fails this test) + div.innerHTML = "<table><tr><td style='padding:0;border:0;display:none'></td><td>t</td></tr></table>"; + tds = div.getElementsByTagName( "td" ); + isSupported = ( tds[ 0 ].offsetHeight === 0 ); + + tds[ 0 ].style.display = ""; + tds[ 1 ].style.display = "none"; + + // Check if empty table cells still have offsetWidth/Height + // (IE <= 8 fail this test) + support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); + + // Figure out if the W3C box model works as expected + div.innerHTML = ""; + div.style.width = div.style.paddingLeft = "1px"; + jQuery.boxModel = support.boxModel = div.offsetWidth === 2; + + if ( typeof div.style.zoom !== "undefined" ) { + // Check if natively block-level elements act like inline-block + // elements when setting their display to 'inline' and giving + // them layout + // (IE < 8 does this) + div.style.display = "inline"; + div.style.zoom = 1; + support.inlineBlockNeedsLayout = ( div.offsetWidth === 2 ); + + // Check if elements with layout shrink-wrap their children + // (IE 6 does this) + div.style.display = ""; + div.innerHTML = "<div style='width:4px;'></div>"; + support.shrinkWrapBlocks = ( div.offsetWidth !== 2 ); + } + + div.style.cssText = ptlm + vb; + div.innerHTML = html; + + outer = div.firstChild; + inner = outer.firstChild; + td = outer.nextSibling.firstChild.firstChild; + + offsetSupport = { + doesNotAddBorder: ( inner.offsetTop !== 5 ), + doesAddBorderForTableAndCells: ( td.offsetTop === 5 ) + }; + + inner.style.position = "fixed"; + inner.style.top = "20px"; + + // safari subtracts parent border width here which is 5px + offsetSupport.fixedPosition = ( inner.offsetTop === 20 || inner.offsetTop === 15 ); + inner.style.position = inner.style.top = ""; + + outer.style.overflow = "hidden"; + outer.style.position = "relative"; + + offsetSupport.subtractsBorderForOverflowNotVisible = ( inner.offsetTop === -5 ); + offsetSupport.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== conMarginTop ); + + body.removeChild( container ); + div = container = null; + + jQuery.extend( support, offsetSupport ); + }); + + return support; +})(); + + + + +var rbrace = /^(?:\{.*\}|\[.*\])$/, + rmultiDash = /([A-Z])/g; + +jQuery.extend({ + cache: {}, + + // Please use with caution + uuid: 0, + + // Unique for each copy of jQuery on the page + // Non-digits removed to match rinlinejQuery + expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ), + + // The following elements throw uncatchable exceptions if you + // attempt to add expando properties to them. + noData: { + "embed": true, + // Ban all objects except for Flash (which handle expandos) + "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", + "applet": true + }, + + hasData: function( elem ) { + elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; + return !!elem && !isEmptyDataObject( elem ); + }, + + data: function( elem, name, data, pvt /* Internal Use Only */ ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var privateCache, thisCache, ret, + internalKey = jQuery.expando, + getByName = typeof name === "string", + + // We have to handle DOM nodes and JS objects differently because IE6-7 + // can't GC object references properly across the DOM-JS boundary + isNode = elem.nodeType, + + // Only DOM nodes need the global jQuery cache; JS object data is + // attached directly to the object so GC can occur automatically + cache = isNode ? jQuery.cache : elem, + + // Only defining an ID for JS objects if its cache already exists allows + // the code to shortcut on the same path as a DOM node with no cache + id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey, + isEvents = name === "events"; + + // Avoid doing any more work than we need to when trying to get data on an + // object that has no data at all + if ( (!id || !cache[id] || (!isEvents && !pvt && !cache[id].data)) && getByName && data === undefined ) { + return; + } + + if ( !id ) { + // Only DOM nodes need a new unique ID for each element since their data + // ends up in the global cache + if ( isNode ) { + elem[ internalKey ] = id = ++jQuery.uuid; + } else { + id = internalKey; + } + } + + if ( !cache[ id ] ) { + cache[ id ] = {}; + + // Avoids exposing jQuery metadata on plain JS objects when the object + // is serialized using JSON.stringify + if ( !isNode ) { + cache[ id ].toJSON = jQuery.noop; + } + } + + // An object can be passed to jQuery.data instead of a key/value pair; this gets + // shallow copied over onto the existing cache + if ( typeof name === "object" || typeof name === "function" ) { + if ( pvt ) { + cache[ id ] = jQuery.extend( cache[ id ], name ); + } else { + cache[ id ].data = jQuery.extend( cache[ id ].data, name ); + } + } + + privateCache = thisCache = cache[ id ]; + + // jQuery data() is stored in a separate object inside the object's internal data + // cache in order to avoid key collisions between internal data and user-defined + // data. + if ( !pvt ) { + if ( !thisCache.data ) { + thisCache.data = {}; + } + + thisCache = thisCache.data; + } + + if ( data !== undefined ) { + thisCache[ jQuery.camelCase( name ) ] = data; + } + + // Users should not attempt to inspect the internal events object using jQuery.data, + // it is undocumented and subject to change. But does anyone listen? No. + if ( isEvents && !thisCache[ name ] ) { + return privateCache.events; + } + + // Check for both converted-to-camel and non-converted data property names + // If a data property was specified + if ( getByName ) { + + // First Try to find as-is property data + ret = thisCache[ name ]; + + // Test for null|undefined property data + if ( ret == null ) { + + // Try to find the camelCased property + ret = thisCache[ jQuery.camelCase( name ) ]; + } + } else { + ret = thisCache; + } + + return ret; + }, + + removeData: function( elem, name, pvt /* Internal Use Only */ ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var thisCache, i, l, + + // Reference to internal data cache key + internalKey = jQuery.expando, + + isNode = elem.nodeType, + + // See jQuery.data for more information + cache = isNode ? jQuery.cache : elem, + + // See jQuery.data for more information + id = isNode ? elem[ internalKey ] : internalKey; + + // If there is already no cache entry for this object, there is no + // purpose in continuing + if ( !cache[ id ] ) { + return; + } + + if ( name ) { + + thisCache = pvt ? cache[ id ] : cache[ id ].data; + + if ( thisCache ) { + + // Support array or space separated string names for data keys + if ( !jQuery.isArray( name ) ) { + + // try the string as a key before any manipulation + if ( name in thisCache ) { + name = [ name ]; + } else { + + // split the camel cased version by spaces unless a key with the spaces exists + name = jQuery.camelCase( name ); + if ( name in thisCache ) { + name = [ name ]; + } else { + name = name.split( " " ); + } + } + } + + for ( i = 0, l = name.length; i < l; i++ ) { + delete thisCache[ name[i] ]; + } + + // If there is no data left in the cache, we want to continue + // and let the cache object itself get destroyed + if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) { + return; + } + } + } + + // See jQuery.data for more information + if ( !pvt ) { + delete cache[ id ].data; + + // Don't destroy the parent cache unless the internal data object + // had been the only thing left in it + if ( !isEmptyDataObject(cache[ id ]) ) { + return; + } + } + + // Browsers that fail expando deletion also refuse to delete expandos on + // the window, but it will allow it on all other JS objects; other browsers + // don't care + // Ensure that `cache` is not a window object #10080 + if ( jQuery.support.deleteExpando || !cache.setInterval ) { + delete cache[ id ]; + } else { + cache[ id ] = null; + } + + // We destroyed the cache and need to eliminate the expando on the node to avoid + // false lookups in the cache for entries that no longer exist + if ( isNode ) { + // IE does not allow us to delete expando properties from nodes, + // nor does it have a removeAttribute function on Document nodes; + // we must handle all of these cases + if ( jQuery.support.deleteExpando ) { + delete elem[ internalKey ]; + } else if ( elem.removeAttribute ) { + elem.removeAttribute( internalKey ); + } else { + elem[ internalKey ] = null; + } + } + }, + + // For internal use only. + _data: function( elem, name, data ) { + return jQuery.data( elem, name, data, true ); + }, + + // A method for determining if a DOM node can handle the data expando + acceptData: function( elem ) { + if ( elem.nodeName ) { + var match = jQuery.noData[ elem.nodeName.toLowerCase() ]; + + if ( match ) { + return !(match === true || elem.getAttribute("classid") !== match); + } + } + + return true; + } +}); + +jQuery.fn.extend({ + data: function( key, value ) { + var parts, attr, name, + data = null; + + if ( typeof key === "undefined" ) { + if ( this.length ) { + data = jQuery.data( this[0] ); + + if ( this[0].nodeType === 1 && !jQuery._data( this[0], "parsedAttrs" ) ) { + attr = this[0].attributes; + for ( var i = 0, l = attr.length; i < l; i++ ) { + name = attr[i].name; + + if ( name.indexOf( "data-" ) === 0 ) { + name = jQuery.camelCase( name.substring(5) ); + + dataAttr( this[0], name, data[ name ] ); + } + } + jQuery._data( this[0], "parsedAttrs", true ); + } + } + + return data; + + } else if ( typeof key === "object" ) { + return this.each(function() { + jQuery.data( this, key ); + }); + } + + parts = key.split("."); + parts[1] = parts[1] ? "." + parts[1] : ""; + + if ( value === undefined ) { + data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]); + + // Try to fetch any internally stored data first + if ( data === undefined && this.length ) { + data = jQuery.data( this[0], key ); + data = dataAttr( this[0], key, data ); + } + + return data === undefined && parts[1] ? + this.data( parts[0] ) : + data; + + } else { + return this.each(function() { + var self = jQuery( this ), + args = [ parts[0], value ]; + + self.triggerHandler( "setData" + parts[1] + "!", args ); + jQuery.data( this, key, value ); + self.triggerHandler( "changeData" + parts[1] + "!", args ); + }); + } + }, + + removeData: function( key ) { + return this.each(function() { + jQuery.removeData( this, key ); + }); + } +}); + +function dataAttr( elem, key, data ) { + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + + var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); + + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + jQuery.isNumeric( data ) ? parseFloat( data ) : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch( e ) {} + + // Make sure we set the data so it isn't changed later + jQuery.data( elem, key, data ); + + } else { + data = undefined; + } + } + + return data; +} + +// checks a cache object for emptiness +function isEmptyDataObject( obj ) { + for ( var name in obj ) { + + // if the public data object is empty, the private is still empty + if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { + continue; + } + if ( name !== "toJSON" ) { + return false; + } + } + + return true; +} + + + + +function handleQueueMarkDefer( elem, type, src ) { + var deferDataKey = type + "defer", + queueDataKey = type + "queue", + markDataKey = type + "mark", + defer = jQuery._data( elem, deferDataKey ); + if ( defer && + ( src === "queue" || !jQuery._data(elem, queueDataKey) ) && + ( src === "mark" || !jQuery._data(elem, markDataKey) ) ) { + // Give room for hard-coded callbacks to fire first + // and eventually mark/queue something else on the element + setTimeout( function() { + if ( !jQuery._data( elem, queueDataKey ) && + !jQuery._data( elem, markDataKey ) ) { + jQuery.removeData( elem, deferDataKey, true ); + defer.fire(); + } + }, 0 ); + } +} + +jQuery.extend({ + + _mark: function( elem, type ) { + if ( elem ) { + type = ( type || "fx" ) + "mark"; + jQuery._data( elem, type, (jQuery._data( elem, type ) || 0) + 1 ); + } + }, + + _unmark: function( force, elem, type ) { + if ( force !== true ) { + type = elem; + elem = force; + force = false; + } + if ( elem ) { + type = type || "fx"; + var key = type + "mark", + count = force ? 0 : ( (jQuery._data( elem, key ) || 1) - 1 ); + if ( count ) { + jQuery._data( elem, key, count ); + } else { + jQuery.removeData( elem, key, true ); + handleQueueMarkDefer( elem, type, "mark" ); + } + } + }, + + queue: function( elem, type, data ) { + var q; + if ( elem ) { + type = ( type || "fx" ) + "queue"; + q = jQuery._data( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !q || jQuery.isArray(data) ) { + q = jQuery._data( elem, type, jQuery.makeArray(data) ); + } else { + q.push( data ); + } + } + return q || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + fn = queue.shift(), + hooks = {}; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + } + + if ( fn ) { + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + jQuery._data( elem, type + ".run", hooks ); + fn.call( elem, function() { + jQuery.dequeue( elem, type ); + }, hooks ); + } + + if ( !queue.length ) { + jQuery.removeData( elem, type + "queue " + type + ".run", true ); + handleQueueMarkDefer( elem, type, "queue" ); + } + } +}); + +jQuery.fn.extend({ + queue: function( type, data ) { + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + } + + if ( data === undefined ) { + return jQuery.queue( this[0], type ); + } + return this.each(function() { + var queue = jQuery.queue( this, type, data ); + + if ( type === "fx" && queue[0] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + }); + }, + dequeue: function( type ) { + return this.each(function() { + jQuery.dequeue( this, type ); + }); + }, + // Based off of the plugin by Clint Helfers, with permission. + // http://blindsignals.com/index.php/2009/07/jquery-delay/ + delay: function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; + type = type || "fx"; + + return this.queue( type, function( next, hooks ) { + var timeout = setTimeout( next, time ); + hooks.stop = function() { + clearTimeout( timeout ); + }; + }); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, object ) { + if ( typeof type !== "string" ) { + object = type; + type = undefined; + } + type = type || "fx"; + var defer = jQuery.Deferred(), + elements = this, + i = elements.length, + count = 1, + deferDataKey = type + "defer", + queueDataKey = type + "queue", + markDataKey = type + "mark", + tmp; + function resolve() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + } + while( i-- ) { + if (( tmp = jQuery.data( elements[ i ], deferDataKey, undefined, true ) || + ( jQuery.data( elements[ i ], queueDataKey, undefined, true ) || + jQuery.data( elements[ i ], markDataKey, undefined, true ) ) && + jQuery.data( elements[ i ], deferDataKey, jQuery.Callbacks( "once memory" ), true ) )) { + count++; + tmp.add( resolve ); + } + } + resolve(); + return defer.promise(); + } +}); + + + + +var rclass = /[\n\t\r]/g, + rspace = /\s+/, + rreturn = /\r/g, + rtype = /^(?:button|input)$/i, + rfocusable = /^(?:button|input|object|select|textarea)$/i, + rclickable = /^a(?:rea)?$/i, + rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i, + getSetAttribute = jQuery.support.getSetAttribute, + nodeHook, boolHook, fixSpecified; + +jQuery.fn.extend({ + attr: function( name, value ) { + return jQuery.access( this, name, value, true, jQuery.attr ); + }, + + removeAttr: function( name ) { + return this.each(function() { + jQuery.removeAttr( this, name ); + }); + }, + + prop: function( name, value ) { + return jQuery.access( this, name, value, true, jQuery.prop ); + }, + + removeProp: function( name ) { + name = jQuery.propFix[ name ] || name; + return this.each(function() { + // try/catch handles cases where IE balks (such as removing a property on window) + try { + this[ name ] = undefined; + delete this[ name ]; + } catch( e ) {} + }); + }, + + addClass: function( value ) { + var classNames, i, l, elem, + setClass, c, cl; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).addClass( value.call(this, j, this.className) ); + }); + } + + if ( value && typeof value === "string" ) { + classNames = value.split( rspace ); + + for ( i = 0, l = this.length; i < l; i++ ) { + elem = this[ i ]; + + if ( elem.nodeType === 1 ) { + if ( !elem.className && classNames.length === 1 ) { + elem.className = value; + + } else { + setClass = " " + elem.className + " "; + + for ( c = 0, cl = classNames.length; c < cl; c++ ) { + if ( !~setClass.indexOf( " " + classNames[ c ] + " " ) ) { + setClass += classNames[ c ] + " "; + } + } + elem.className = jQuery.trim( setClass ); + } + } + } + } + + return this; + }, + + removeClass: function( value ) { + var classNames, i, l, elem, className, c, cl; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).removeClass( value.call(this, j, this.className) ); + }); + } + + if ( (value && typeof value === "string") || value === undefined ) { + classNames = ( value || "" ).split( rspace ); + + for ( i = 0, l = this.length; i < l; i++ ) { + elem = this[ i ]; + + if ( elem.nodeType === 1 && elem.className ) { + if ( value ) { + className = (" " + elem.className + " ").replace( rclass, " " ); + for ( c = 0, cl = classNames.length; c < cl; c++ ) { + className = className.replace(" " + classNames[ c ] + " ", " "); + } + elem.className = jQuery.trim( className ); + + } else { + elem.className = ""; + } + } + } + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var type = typeof value, + isBool = typeof stateVal === "boolean"; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( i ) { + jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); + }); + } + + return this.each(function() { + if ( type === "string" ) { + // toggle individual class names + var className, + i = 0, + self = jQuery( this ), + state = stateVal, + classNames = value.split( rspace ); + + while ( (className = classNames[ i++ ]) ) { + // check each className given, space seperated list + state = isBool ? state : !self.hasClass( className ); + self[ state ? "addClass" : "removeClass" ]( className ); + } + + } else if ( type === "undefined" || type === "boolean" ) { + if ( this.className ) { + // store className if set + jQuery._data( this, "__className__", this.className ); + } + + // toggle whole className + this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; + } + }); + }, + + hasClass: function( selector ) { + var className = " " + selector + " ", + i = 0, + l = this.length; + for ( ; i < l; i++ ) { + if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) { + return true; + } + } + + return false; + }, + + val: function( value ) { + var hooks, ret, isFunction, + elem = this[0]; + + if ( !arguments.length ) { + if ( elem ) { + hooks = jQuery.valHooks[ elem.nodeName.toLowerCase() ] || jQuery.valHooks[ elem.type ]; + + if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { + return ret; + } + + ret = elem.value; + + return typeof ret === "string" ? + // handle most common string cases + ret.replace(rreturn, "") : + // handle cases where value is null/undef or number + ret == null ? "" : ret; + } + + return; + } + + isFunction = jQuery.isFunction( value ); + + return this.each(function( i ) { + var self = jQuery(this), val; + + if ( this.nodeType !== 1 ) { + return; + } + + if ( isFunction ) { + val = value.call( this, i, self.val() ); + } else { + val = value; + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + } else if ( typeof val === "number" ) { + val += ""; + } else if ( jQuery.isArray( val ) ) { + val = jQuery.map(val, function ( value ) { + return value == null ? "" : value + ""; + }); + } + + hooks = jQuery.valHooks[ this.nodeName.toLowerCase() ] || jQuery.valHooks[ this.type ]; + + // If set returns undefined, fall back to normal setting + if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; + } + }); + } +}); + +jQuery.extend({ + valHooks: { + option: { + get: function( elem ) { + // attributes.value is undefined in Blackberry 4.7 but + // uses .value. See #6932 + var val = elem.attributes.value; + return !val || val.specified ? elem.value : elem.text; + } + }, + select: { + get: function( elem ) { + var value, i, max, option, + index = elem.selectedIndex, + values = [], + options = elem.options, + one = elem.type === "select-one"; + + // Nothing was selected + if ( index < 0 ) { + return null; + } + + // Loop through all the selected options + i = one ? index : 0; + max = one ? index + 1 : options.length; + for ( ; i < max; i++ ) { + option = options[ i ]; + + // Don't return options that are disabled or in a disabled optgroup + if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) && + (!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) { + + // Get the specific value for the option + value = jQuery( option ).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + // Fixes Bug #2551 -- select.val() broken in IE after form.reset() + if ( one && !values.length && options.length ) { + return jQuery( options[ index ] ).val(); + } + + return values; + }, + + set: function( elem, value ) { + var values = jQuery.makeArray( value ); + + jQuery(elem).find("option").each(function() { + this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; + }); + + if ( !values.length ) { + elem.selectedIndex = -1; + } + return values; + } + } + }, + + attrFn: { + val: true, + css: true, + html: true, + text: true, + data: true, + width: true, + height: true, + offset: true + }, + + attr: function( elem, name, value, pass ) { + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set attributes on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + if ( pass && name in jQuery.attrFn ) { + return jQuery( elem )[ name ]( value ); + } + + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === "undefined" ) { + return jQuery.prop( elem, name, value ); + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + // All attributes are lowercase + // Grab necessary hook if one is defined + if ( notxml ) { + name = name.toLowerCase(); + hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook ); + } + + if ( value !== undefined ) { + + if ( value === null ) { + jQuery.removeAttr( elem, name ); + return; + + } else if ( hooks && "set" in hooks && notxml && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + elem.setAttribute( name, "" + value ); + return value; + } + + } else if ( hooks && "get" in hooks && notxml && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + + ret = elem.getAttribute( name ); + + // Non-existent attributes return null, we normalize to undefined + return ret === null ? + undefined : + ret; + } + }, + + removeAttr: function( elem, value ) { + var propName, attrNames, name, l, + i = 0; + + if ( value && elem.nodeType === 1 ) { + attrNames = value.toLowerCase().split( rspace ); + l = attrNames.length; + + for ( ; i < l; i++ ) { + name = attrNames[ i ]; + + if ( name ) { + propName = jQuery.propFix[ name ] || name; + + // See #9699 for explanation of this approach (setting first, then removal) + jQuery.attr( elem, name, "" ); + elem.removeAttribute( getSetAttribute ? name : propName ); + + // Set corresponding property to false for boolean attributes + if ( rboolean.test( name ) && propName in elem ) { + elem[ propName ] = false; + } + } + } + } + }, + + attrHooks: { + type: { + set: function( elem, value ) { + // We can't allow the type property to be changed (since it causes problems in IE) + if ( rtype.test( elem.nodeName ) && elem.parentNode ) { + jQuery.error( "type property can't be changed" ); + } else if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { + // Setting the type on a radio button after the value resets the value in IE6-9 + // Reset value to it's default in case type is set after value + // This is for element creation + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + }, + // Use the value property for back compat + // Use the nodeHook for button elements in IE6/7 (#1954) + value: { + get: function( elem, name ) { + if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { + return nodeHook.get( elem, name ); + } + return name in elem ? + elem.value : + null; + }, + set: function( elem, value, name ) { + if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { + return nodeHook.set( elem, value, name ); + } + // Does not return so that setAttribute is also used + elem.value = value; + } + } + }, + + propFix: { + tabindex: "tabIndex", + readonly: "readOnly", + "for": "htmlFor", + "class": "className", + maxlength: "maxLength", + cellspacing: "cellSpacing", + cellpadding: "cellPadding", + rowspan: "rowSpan", + colspan: "colSpan", + usemap: "useMap", + frameborder: "frameBorder", + contenteditable: "contentEditable" + }, + + prop: function( elem, name, value ) { + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set properties on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + if ( notxml ) { + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + return ( elem[ name ] = value ); + } + + } else { + if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + return elem[ name ]; + } + } + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set + // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + var attributeNode = elem.getAttributeNode("tabindex"); + + return attributeNode && attributeNode.specified ? + parseInt( attributeNode.value, 10 ) : + rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? + 0 : + undefined; + } + } + } +}); + +// Add the tabIndex propHook to attrHooks for back-compat (different case is intentional) +jQuery.attrHooks.tabindex = jQuery.propHooks.tabIndex; + +// Hook for boolean attributes +boolHook = { + get: function( elem, name ) { + // Align boolean attributes with corresponding properties + // Fall back to attribute presence where some booleans are not supported + var attrNode, + property = jQuery.prop( elem, name ); + return property === true || typeof property !== "boolean" && ( attrNode = elem.getAttributeNode(name) ) && attrNode.nodeValue !== false ? + name.toLowerCase() : + undefined; + }, + set: function( elem, value, name ) { + var propName; + if ( value === false ) { + // Remove boolean attributes when set to false + jQuery.removeAttr( elem, name ); + } else { + // value is true since we know at this point it's type boolean and not false + // Set boolean attributes to the same name and set the DOM property + propName = jQuery.propFix[ name ] || name; + if ( propName in elem ) { + // Only set the IDL specifically if it already exists on the element + elem[ propName ] = true; + } + + elem.setAttribute( name, name.toLowerCase() ); + } + return name; + } +}; + +// IE6/7 do not support getting/setting some attributes with get/setAttribute +if ( !getSetAttribute ) { + + fixSpecified = { + name: true, + id: true + }; + + // Use this for any attribute in IE6/7 + // This fixes almost every IE6/7 issue + nodeHook = jQuery.valHooks.button = { + get: function( elem, name ) { + var ret; + ret = elem.getAttributeNode( name ); + return ret && ( fixSpecified[ name ] ? ret.nodeValue !== "" : ret.specified ) ? + ret.nodeValue : + undefined; + }, + set: function( elem, value, name ) { + // Set the existing or create a new attribute node + var ret = elem.getAttributeNode( name ); + if ( !ret ) { + ret = document.createAttribute( name ); + elem.setAttributeNode( ret ); + } + return ( ret.nodeValue = value + "" ); + } + }; + + // Apply the nodeHook to tabindex + jQuery.attrHooks.tabindex.set = nodeHook.set; + + // Set width and height to auto instead of 0 on empty string( Bug #8150 ) + // This is for removals + jQuery.each([ "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + set: function( elem, value ) { + if ( value === "" ) { + elem.setAttribute( name, "auto" ); + return value; + } + } + }); + }); + + // Set contenteditable to false on removals(#10429) + // Setting to empty string throws an error as an invalid value + jQuery.attrHooks.contenteditable = { + get: nodeHook.get, + set: function( elem, value, name ) { + if ( value === "" ) { + value = "false"; + } + nodeHook.set( elem, value, name ); + } + }; +} + + +// Some attributes require a special call on IE +if ( !jQuery.support.hrefNormalized ) { + jQuery.each([ "href", "src", "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + get: function( elem ) { + var ret = elem.getAttribute( name, 2 ); + return ret === null ? undefined : ret; + } + }); + }); +} + +if ( !jQuery.support.style ) { + jQuery.attrHooks.style = { + get: function( elem ) { + // Return undefined in the case of empty string + // Normalize to lowercase since IE uppercases css property names + return elem.style.cssText.toLowerCase() || undefined; + }, + set: function( elem, value ) { + return ( elem.style.cssText = "" + value ); + } + }; +} + +// Safari mis-reports the default selected property of an option +// Accessing the parent's selectedIndex property fixes it +if ( !jQuery.support.optSelected ) { + jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, { + get: function( elem ) { + var parent = elem.parentNode; + + if ( parent ) { + parent.selectedIndex; + + // Make sure that it also works with optgroups, see #5701 + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + return null; + } + }); +} + +// IE6/7 call enctype encoding +if ( !jQuery.support.enctype ) { + jQuery.propFix.enctype = "encoding"; +} + +// Radios and checkboxes getter/setter +if ( !jQuery.support.checkOn ) { + jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + get: function( elem ) { + // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified + return elem.getAttribute("value") === null ? "on" : elem.value; + } + }; + }); +} +jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], { + set: function( elem, value ) { + if ( jQuery.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); + } + } + }); +}); + + + + +var rformElems = /^(?:textarea|input|select)$/i, + rtypenamespace = /^([^\.]*)?(?:\.(.+))?$/, + rhoverHack = /\bhover(\.\S+)?\b/, + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|contextmenu)|click/, + rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + rquickIs = /^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/, + quickParse = function( selector ) { + var quick = rquickIs.exec( selector ); + if ( quick ) { + // 0 1 2 3 + // [ _, tag, id, class ] + quick[1] = ( quick[1] || "" ).toLowerCase(); + quick[3] = quick[3] && new RegExp( "(?:^|\\s)" + quick[3] + "(?:\\s|$)" ); + } + return quick; + }, + quickIs = function( elem, m ) { + var attrs = elem.attributes || {}; + return ( + (!m[1] || elem.nodeName.toLowerCase() === m[1]) && + (!m[2] || (attrs.id || {}).value === m[2]) && + (!m[3] || m[3].test( (attrs[ "class" ] || {}).value )) + ); + }, + hoverHack = function( events ) { + return jQuery.event.special.hover ? events : events.replace( rhoverHack, "mouseenter$1 mouseleave$1" ); + }; + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + add: function( elem, types, handler, data, selector ) { + + var elemData, eventHandle, events, + t, tns, type, namespaces, handleObj, + handleObjIn, quick, handlers, special; + + // Don't attach events to noData or text/comment nodes (allow plain objects tho) + if ( elem.nodeType === 3 || elem.nodeType === 8 || !types || !handler || !(elemData = jQuery._data( elem )) ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + events = elemData.events; + if ( !events ) { + elemData.events = events = {}; + } + eventHandle = elemData.handle; + if ( !eventHandle ) { + elemData.handle = eventHandle = function( e ) { + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ? + jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : + undefined; + }; + // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events + eventHandle.elem = elem; + } + + // Handle multiple events separated by a space + // jQuery(...).bind("mouseover mouseout", fn); + types = jQuery.trim( hoverHack(types) ).split( " " ); + for ( t = 0; t < types.length; t++ ) { + + tns = rtypenamespace.exec( types[t] ) || []; + type = tns[1]; + namespaces = ( tns[2] || "" ).split( "." ).sort(); + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend({ + type: type, + origType: tns[1], + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + quick: quickParse( selector ), + namespace: namespaces.join(".") + }, handleObjIn ); + + // Init the event handler queue if we're the first + handlers = events[ type ]; + if ( !handlers ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener/attachEvent if the special events handler returns false + if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + // Bind the global event handler to the element + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle, false ); + + } else if ( elem.attachEvent ) { + elem.attachEvent( "on" + type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + // Nullify elem to prevent memory leaks in IE + elem = null; + }, + + global: {}, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + + var elemData = jQuery.hasData( elem ) && jQuery._data( elem ), + t, tns, type, origType, namespaces, origCount, + j, events, special, handle, eventType, handleObj; + + if ( !elemData || !(events = elemData.events) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = jQuery.trim( hoverHack( types || "" ) ).split(" "); + for ( t = 0; t < types.length; t++ ) { + tns = rtypenamespace.exec( types[t] ) || []; + type = origType = tns[1]; + namespaces = tns[2]; + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector? special.delegateType : special.bindType ) || type; + eventType = events[ type ] || []; + origCount = eventType.length; + namespaces = namespaces ? new RegExp("(^|\\.)" + namespaces.split(".").sort().join("\\.(?:.*\\.)?") + "(\\.|$)") : null; + + // Remove matching events + for ( j = 0; j < eventType.length; j++ ) { + handleObj = eventType[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !namespaces || namespaces.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { + eventType.splice( j--, 1 ); + + if ( handleObj.selector ) { + eventType.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( eventType.length === 0 && origCount !== eventType.length ) { + if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) { + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + handle = elemData.handle; + if ( handle ) { + handle.elem = null; + } + + // removeData also checks for emptiness and clears the expando if empty + // so use it instead of delete + jQuery.removeData( elem, [ "events", "handle" ], true ); + } + }, + + // Events that are safe to short-circuit if no handlers are attached. + // Native DOM events should not be added, they may have inline handlers. + customEvent: { + "getData": true, + "setData": true, + "changeData": true + }, + + trigger: function( event, data, elem, onlyHandlers ) { + // Don't do events on text and comment nodes + if ( elem && (elem.nodeType === 3 || elem.nodeType === 8) ) { + return; + } + + // Event object or event type + var type = event.type || event, + namespaces = [], + cache, exclusive, i, cur, old, ontype, special, handle, eventPath, bubbleType; + + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf( "!" ) >= 0 ) { + // Exclusive events trigger only for the exact event (no namespaces) + type = type.slice(0, -1); + exclusive = true; + } + + if ( type.indexOf( "." ) >= 0 ) { + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split("."); + type = namespaces.shift(); + namespaces.sort(); + } + + if ( (!elem || jQuery.event.customEvent[ type ]) && !jQuery.event.global[ type ] ) { + // No jQuery handlers for this event type, and it can't have inline handlers + return; + } + + // Caller can pass in an Event, Object, or just an event type string + event = typeof event === "object" ? + // jQuery.Event object + event[ jQuery.expando ] ? event : + // Object literal + new jQuery.Event( type, event ) : + // Just the event type (string) + new jQuery.Event( type ); + + event.type = type; + event.isTrigger = true; + event.exclusive = exclusive; + event.namespace = namespaces.join( "." ); + event.namespace_re = event.namespace? new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.)?") + "(\\.|$)") : null; + ontype = type.indexOf( ":" ) < 0 ? "on" + type : ""; + + // Handle a global trigger + if ( !elem ) { + + // TODO: Stop taunting the data cache; remove global events and always attach to document + cache = jQuery.cache; + for ( i in cache ) { + if ( cache[ i ].events && cache[ i ].events[ type ] ) { + jQuery.event.trigger( event, data, cache[ i ].handle.elem, true ); + } + } + return; + } + + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data != null ? jQuery.makeArray( data ) : []; + data.unshift( event ); + + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + eventPath = [[ elem, special.bindType || type ]]; + if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + cur = rfocusMorph.test( bubbleType + type ) ? elem : elem.parentNode; + old = null; + for ( ; cur; cur = cur.parentNode ) { + eventPath.push([ cur, bubbleType ]); + old = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( old && old === elem.ownerDocument ) { + eventPath.push([ old.defaultView || old.parentWindow || window, bubbleType ]); + } + } + + // Fire handlers on the event path + for ( i = 0; i < eventPath.length && !event.isPropagationStopped(); i++ ) { + + cur = eventPath[i][0]; + event.type = eventPath[i][1]; + + handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + // Note that this is a bare JS function and not a jQuery handler + handle = ontype && cur[ ontype ]; + if ( handle && jQuery.acceptData( cur ) && handle.apply( cur, data ) === false ) { + event.preventDefault(); + } + } + event.type = type; + + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { + + if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) && + !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) { + + // Call a native DOM method on the target with the same name name as the event. + // Can't use an .isFunction() check here because IE6/7 fails that test. + // Don't do default actions on window, that's where global variables be (#6170) + // IE<9 dies on focus/blur to hidden element (#1486) + if ( ontype && elem[ type ] && ((type !== "focus" && type !== "blur") || event.target.offsetWidth !== 0) && !jQuery.isWindow( elem ) ) { + + // Don't re-trigger an onFOO event when we call its FOO() method + old = elem[ ontype ]; + + if ( old ) { + elem[ ontype ] = null; + } + + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + elem[ type ](); + jQuery.event.triggered = undefined; + + if ( old ) { + elem[ ontype ] = old; + } + } + } + } + + return event.result; + }, + + dispatch: function( event ) { + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( event || window.event ); + + var handlers = ( (jQuery._data( this, "events" ) || {} )[ event.type ] || []), + delegateCount = handlers.delegateCount, + args = [].slice.call( arguments, 0 ), + run_all = !event.exclusive && !event.namespace, + handlerQueue = [], + i, j, cur, jqcur, ret, selMatch, matched, matches, handleObj, sel, related; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[0] = event; + event.delegateTarget = this; + + // Determine handlers that should run if there are delegated events + // Avoid disabled elements in IE (#6911) and non-left-click bubbling in Firefox (#3861) + if ( delegateCount && !event.target.disabled && !(event.button && event.type === "click") ) { + + // Pregenerate a single jQuery object for reuse with .is() + jqcur = jQuery(this); + jqcur.context = this.ownerDocument || this; + + for ( cur = event.target; cur != this; cur = cur.parentNode || this ) { + selMatch = {}; + matches = []; + jqcur[0] = cur; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + sel = handleObj.selector; + + if ( selMatch[ sel ] === undefined ) { + selMatch[ sel ] = ( + handleObj.quick ? quickIs( cur, handleObj.quick ) : jqcur.is( sel ) + ); + } + if ( selMatch[ sel ] ) { + matches.push( handleObj ); + } + } + if ( matches.length ) { + handlerQueue.push({ elem: cur, matches: matches }); + } + } + } + + // Add the remaining (directly-bound) handlers + if ( handlers.length > delegateCount ) { + handlerQueue.push({ elem: this, matches: handlers.slice( delegateCount ) }); + } + + // Run delegates first; they may want to stop propagation beneath us + for ( i = 0; i < handlerQueue.length && !event.isPropagationStopped(); i++ ) { + matched = handlerQueue[ i ]; + event.currentTarget = matched.elem; + + for ( j = 0; j < matched.matches.length && !event.isImmediatePropagationStopped(); j++ ) { + handleObj = matched.matches[ j ]; + + // Triggered event must either 1) be non-exclusive and have no namespace, or + // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). + if ( run_all || (!event.namespace && !handleObj.namespace) || event.namespace_re && event.namespace_re.test( handleObj.namespace ) ) { + + event.data = handleObj.data; + event.handleObj = handleObj; + + ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) + .apply( matched.elem, args ); + + if ( ret !== undefined ) { + event.result = ret; + if ( ret === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + return event.result; + }, + + // Includes some event props shared by KeyEvent and MouseEvent + // *** attrChange attrName relatedNode srcElement are not normalized, non-W3C, deprecated, will be removed in 1.8 *** + props: "attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), + + fixHooks: {}, + + keyHooks: { + props: "char charCode key keyCode".split(" "), + filter: function( event, original ) { + + // Add which for key events + if ( event.which == null ) { + event.which = original.charCode != null ? original.charCode : original.keyCode; + } + + return event; + } + }, + + mouseHooks: { + props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), + filter: function( event, original ) { + var eventDoc, doc, body, + button = original.button, + fromElement = original.fromElement; + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && original.clientX != null ) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + + event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); + } + + // Add relatedTarget, if necessary + if ( !event.relatedTarget && fromElement ) { + event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && button !== undefined ) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } + + return event; + } + }, + + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } + + // Create a writable copy of the event object and normalize some properties + var i, prop, + originalEvent = event, + fixHook = jQuery.event.fixHooks[ event.type ] || {}, + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + + event = jQuery.Event( originalEvent ); + + for ( i = copy.length; i; ) { + prop = copy[ --i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Fix target property, if necessary (#1925, IE 6/7/8 & Safari2) + if ( !event.target ) { + event.target = originalEvent.srcElement || document; + } + + // Target should not be a text node (#504, Safari) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + // For mouse/key events; add metaKey if it's not there (#3368, IE6/7/8) + if ( event.metaKey === undefined ) { + event.metaKey = event.ctrlKey; + } + + return fixHook.filter? fixHook.filter( event, originalEvent ) : event; + }, + + special: { + ready: { + // Make sure the ready event is setup + setup: jQuery.bindReady + }, + + load: { + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + + focus: { + delegateType: "focusin" + }, + blur: { + delegateType: "focusout" + }, + + beforeunload: { + setup: function( data, namespaces, eventHandle ) { + // We only want to do this special case on windows + if ( jQuery.isWindow( this ) ) { + this.onbeforeunload = eventHandle; + } + }, + + teardown: function( namespaces, eventHandle ) { + if ( this.onbeforeunload === eventHandle ) { + this.onbeforeunload = null; + } + } + } + }, + + simulate: function( type, elem, event, bubble ) { + // Piggyback on a donor event to simulate a different one. + // Fake originalEvent to avoid donor's stopPropagation, but if the + // simulated event prevents default then we do the same on the donor. + var e = jQuery.extend( + new jQuery.Event(), + event, + { type: type, + isSimulated: true, + originalEvent: {} + } + ); + if ( bubble ) { + jQuery.event.trigger( e, null, elem ); + } else { + jQuery.event.dispatch.call( elem, e ); + } + if ( e.isDefaultPrevented() ) { + event.preventDefault(); + } + } +}; + +// Some plugins are using, but it's undocumented/deprecated and will be removed. +// The 1.7 special event interface should provide all the hooks needed now. +jQuery.event.handle = jQuery.event.dispatch; + +jQuery.removeEvent = document.removeEventListener ? + function( elem, type, handle ) { + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle, false ); + } + } : + function( elem, type, handle ) { + if ( elem.detachEvent ) { + elem.detachEvent( "on" + type, handle ); + } + }; + +jQuery.Event = function( src, props ) { + // Allow instantiation without the 'new' keyword + if ( !(this instanceof jQuery.Event) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false || + src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +function returnFalse() { + return false; +} +function returnTrue() { + return true; +} + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + preventDefault: function() { + this.isDefaultPrevented = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + + // if preventDefault exists run it on the original event + if ( e.preventDefault ) { + e.preventDefault(); + + // otherwise set the returnValue property of the original event to false (IE) + } else { + e.returnValue = false; + } + }, + stopPropagation: function() { + this.isPropagationStopped = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + // if stopPropagation exists run it on the original event + if ( e.stopPropagation ) { + e.stopPropagation(); + } + // otherwise set the cancelBubble property of the original event to true (IE) + e.cancelBubble = true; + }, + stopImmediatePropagation: function() { + this.isImmediatePropagationStopped = returnTrue; + this.stopPropagation(); + }, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse +}; + +// Create mouseenter/leave events using mouseover/out and event-time checks +jQuery.each({ + mouseenter: "mouseover", + mouseleave: "mouseout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var target = this, + related = event.relatedTarget, + handleObj = event.handleObj, + selector = handleObj.selector, + ret; + + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || (related !== target && !jQuery.contains( target, related )) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +}); + +// IE submit delegation +if ( !jQuery.support.submitBubbles ) { + + jQuery.event.special.submit = { + setup: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Lazy-add a submit handler when a descendant form may potentially be submitted + jQuery.event.add( this, "click._submit keypress._submit", function( e ) { + // Node name check avoids a VML-related crash in IE (#9807) + var elem = e.target, + form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; + if ( form && !form._submit_attached ) { + jQuery.event.add( form, "submit._submit", function( event ) { + // If form was submitted by the user, bubble the event up the tree + if ( this.parentNode && !event.isTrigger ) { + jQuery.event.simulate( "submit", this.parentNode, event, true ); + } + }); + form._submit_attached = true; + } + }); + // return undefined since we don't need an event listener + }, + + teardown: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Remove delegated handlers; cleanData eventually reaps submit handlers attached above + jQuery.event.remove( this, "._submit" ); + } + }; +} + +// IE change delegation and checkbox/radio fix +if ( !jQuery.support.changeBubbles ) { + + jQuery.event.special.change = { + + setup: function() { + + if ( rformElems.test( this.nodeName ) ) { + // IE doesn't fire change on a check/radio until blur; trigger it on click + // after a propertychange. Eat the blur-change in special.change.handle. + // This still fires onchange a second time for check/radio after blur. + if ( this.type === "checkbox" || this.type === "radio" ) { + jQuery.event.add( this, "propertychange._change", function( event ) { + if ( event.originalEvent.propertyName === "checked" ) { + this._just_changed = true; + } + }); + jQuery.event.add( this, "click._change", function( event ) { + if ( this._just_changed && !event.isTrigger ) { + this._just_changed = false; + jQuery.event.simulate( "change", this, event, true ); + } + }); + } + return false; + } + // Delegated event; lazy-add a change handler on descendant inputs + jQuery.event.add( this, "beforeactivate._change", function( e ) { + var elem = e.target; + + if ( rformElems.test( elem.nodeName ) && !elem._change_attached ) { + jQuery.event.add( elem, "change._change", function( event ) { + if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { + jQuery.event.simulate( "change", this.parentNode, event, true ); + } + }); + elem._change_attached = true; + } + }); + }, + + handle: function( event ) { + var elem = event.target; + + // Swallow native change events from checkbox/radio, we already triggered them above + if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { + return event.handleObj.handler.apply( this, arguments ); + } + }, + + teardown: function() { + jQuery.event.remove( this, "._change" ); + + return rformElems.test( this.nodeName ); + } + }; +} + +// Create "bubbling" focus and blur events +if ( !jQuery.support.focusinBubbles ) { + jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { + + // Attach a single capturing handler while someone wants focusin/focusout + var attaches = 0, + handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); + }; + + jQuery.event.special[ fix ] = { + setup: function() { + if ( attaches++ === 0 ) { + document.addEventListener( orig, handler, true ); + } + }, + teardown: function() { + if ( --attaches === 0 ) { + document.removeEventListener( orig, handler, true ); + } + } + }; + }); +} + +jQuery.fn.extend({ + + on: function( types, selector, data, fn, /*INTERNAL*/ one ) { + var origFn, type; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + // ( types-Object, data ) + data = selector; + selector = undefined; + } + for ( type in types ) { + this.on( type, selector, data, types[ type ], one ); + } + return this; + } + + if ( data == null && fn == null ) { + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return this; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return this.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + }); + }, + one: function( types, selector, data, fn ) { + return this.on.call( this, types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + if ( types && types.preventDefault && types.handleObj ) { + // ( event ) dispatched jQuery.Event + var handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace? handleObj.type + "." + handleObj.namespace : handleObj.type, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + // ( types-object [, selector] ) + for ( var type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each(function() { + jQuery.event.remove( this, types, fn, selector ); + }); + }, + + bind: function( types, data, fn ) { + return this.on( types, null, data, fn ); + }, + unbind: function( types, fn ) { + return this.off( types, null, fn ); + }, + + live: function( types, data, fn ) { + jQuery( this.context ).on( types, this.selector, data, fn ); + return this; + }, + die: function( types, fn ) { + jQuery( this.context ).off( types, this.selector || "**", fn ); + return this; + }, + + delegate: function( selector, types, data, fn ) { + return this.on( types, selector, data, fn ); + }, + undelegate: function( selector, types, fn ) { + // ( namespace ) or ( selector, types [, fn] ) + return arguments.length == 1? this.off( selector, "**" ) : this.off( types, selector, fn ); + }, + + trigger: function( type, data ) { + return this.each(function() { + jQuery.event.trigger( type, data, this ); + }); + }, + triggerHandler: function( type, data ) { + if ( this[0] ) { + return jQuery.event.trigger( type, data, this[0], true ); + } + }, + + toggle: function( fn ) { + // Save reference to arguments for access in closure + var args = arguments, + guid = fn.guid || jQuery.guid++, + i = 0, + toggler = function( event ) { + // Figure out which function to execute + var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i; + jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 ); + + // Make sure that clicks stop + event.preventDefault(); + + // and execute the function + return args[ lastToggle ].apply( this, arguments ) || false; + }; + + // link all the functions, so any of them can unbind this click handler + toggler.guid = guid; + while ( i < args.length ) { + args[ i++ ].guid = guid; + } + + return this.click( toggler ); + }, + + hover: function( fnOver, fnOut ) { + return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); + } +}); + +jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + + "change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) { + + // Handle event binding + jQuery.fn[ name ] = function( data, fn ) { + if ( fn == null ) { + fn = data; + data = null; + } + + return arguments.length > 0 ? + this.on( name, null, data, fn ) : + this.trigger( name ); + }; + + if ( jQuery.attrFn ) { + jQuery.attrFn[ name ] = true; + } + + if ( rkeyEvent.test( name ) ) { + jQuery.event.fixHooks[ name ] = jQuery.event.keyHooks; + } + + if ( rmouseEvent.test( name ) ) { + jQuery.event.fixHooks[ name ] = jQuery.event.mouseHooks; + } +}); + + + +/*! + * Sizzle CSS Selector Engine + * Copyright 2011, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * More information: http://sizzlejs.com/ + */ +(function(){ + +var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, + expando = "sizcache" + (Math.random() + '').replace('.', ''), + done = 0, + toString = Object.prototype.toString, + hasDuplicate = false, + baseHasDuplicate = true, + rBackslash = /\\/g, + rReturn = /\r\n/g, + rNonWord = /\W/; + +// Here we check if the JavaScript engine is using some sort of +// optimization where it does not always call our comparision +// function. If that is the case, discard the hasDuplicate value. +// Thus far that includes Google Chrome. +[0, 0].sort(function() { + baseHasDuplicate = false; + return 0; +}); + +var Sizzle = function( selector, context, results, seed ) { + results = results || []; + context = context || document; + + var origContext = context; + + if ( context.nodeType !== 1 && context.nodeType !== 9 ) { + return []; + } + + if ( !selector || typeof selector !== "string" ) { + return results; + } + + var m, set, checkSet, extra, ret, cur, pop, i, + prune = true, + contextXML = Sizzle.isXML( context ), + parts = [], + soFar = selector; + + // Reset the position of the chunker regexp (start from head) + do { + chunker.exec( "" ); + m = chunker.exec( soFar ); + + if ( m ) { + soFar = m[3]; + + parts.push( m[1] ); + + if ( m[2] ) { + extra = m[3]; + break; + } + } + } while ( m ); + + if ( parts.length > 1 && origPOS.exec( selector ) ) { + + if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { + set = posProcess( parts[0] + parts[1], context, seed ); + + } else { + set = Expr.relative[ parts[0] ] ? + [ context ] : + Sizzle( parts.shift(), context ); + + while ( parts.length ) { + selector = parts.shift(); + + if ( Expr.relative[ selector ] ) { + selector += parts.shift(); + } + + set = posProcess( selector, set, seed ); + } + } + + } else { + // Take a shortcut and set the context if the root selector is an ID + // (but not if it'll be faster if the inner selector is an ID) + if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && + Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { + + ret = Sizzle.find( parts.shift(), context, contextXML ); + context = ret.expr ? + Sizzle.filter( ret.expr, ret.set )[0] : + ret.set[0]; + } + + if ( context ) { + ret = seed ? + { expr: parts.pop(), set: makeArray(seed) } : + Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); + + set = ret.expr ? + Sizzle.filter( ret.expr, ret.set ) : + ret.set; + + if ( parts.length > 0 ) { + checkSet = makeArray( set ); + + } else { + prune = false; + } + + while ( parts.length ) { + cur = parts.pop(); + pop = cur; + + if ( !Expr.relative[ cur ] ) { + cur = ""; + } else { + pop = parts.pop(); + } + + if ( pop == null ) { + pop = context; + } + + Expr.relative[ cur ]( checkSet, pop, contextXML ); + } + + } else { + checkSet = parts = []; + } + } + + if ( !checkSet ) { + checkSet = set; + } + + if ( !checkSet ) { + Sizzle.error( cur || selector ); + } + + if ( toString.call(checkSet) === "[object Array]" ) { + if ( !prune ) { + results.push.apply( results, checkSet ); + + } else if ( context && context.nodeType === 1 ) { + for ( i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) { + results.push( set[i] ); + } + } + + } else { + for ( i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && checkSet[i].nodeType === 1 ) { + results.push( set[i] ); + } + } + } + + } else { + makeArray( checkSet, results ); + } + + if ( extra ) { + Sizzle( extra, origContext, results, seed ); + Sizzle.uniqueSort( results ); + } + + return results; +}; + +Sizzle.uniqueSort = function( results ) { + if ( sortOrder ) { + hasDuplicate = baseHasDuplicate; + results.sort( sortOrder ); + + if ( hasDuplicate ) { + for ( var i = 1; i < results.length; i++ ) { + if ( results[i] === results[ i - 1 ] ) { + results.splice( i--, 1 ); + } + } + } + } + + return results; +}; + +Sizzle.matches = function( expr, set ) { + return Sizzle( expr, null, null, set ); +}; + +Sizzle.matchesSelector = function( node, expr ) { + return Sizzle( expr, null, null, [node] ).length > 0; +}; + +Sizzle.find = function( expr, context, isXML ) { + var set, i, len, match, type, left; + + if ( !expr ) { + return []; + } + + for ( i = 0, len = Expr.order.length; i < len; i++ ) { + type = Expr.order[i]; + + if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { + left = match[1]; + match.splice( 1, 1 ); + + if ( left.substr( left.length - 1 ) !== "\\" ) { + match[1] = (match[1] || "").replace( rBackslash, "" ); + set = Expr.find[ type ]( match, context, isXML ); + + if ( set != null ) { + expr = expr.replace( Expr.match[ type ], "" ); + break; + } + } + } + } + + if ( !set ) { + set = typeof context.getElementsByTagName !== "undefined" ? + context.getElementsByTagName( "*" ) : + []; + } + + return { set: set, expr: expr }; +}; + +Sizzle.filter = function( expr, set, inplace, not ) { + var match, anyFound, + type, found, item, filter, left, + i, pass, + old = expr, + result = [], + curLoop = set, + isXMLFilter = set && set[0] && Sizzle.isXML( set[0] ); + + while ( expr && set.length ) { + for ( type in Expr.filter ) { + if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { + filter = Expr.filter[ type ]; + left = match[1]; + + anyFound = false; + + match.splice(1,1); + + if ( left.substr( left.length - 1 ) === "\\" ) { + continue; + } + + if ( curLoop === result ) { + result = []; + } + + if ( Expr.preFilter[ type ] ) { + match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); + + if ( !match ) { + anyFound = found = true; + + } else if ( match === true ) { + continue; + } + } + + if ( match ) { + for ( i = 0; (item = curLoop[i]) != null; i++ ) { + if ( item ) { + found = filter( item, match, i, curLoop ); + pass = not ^ found; + + if ( inplace && found != null ) { + if ( pass ) { + anyFound = true; + + } else { + curLoop[i] = false; + } + + } else if ( pass ) { + result.push( item ); + anyFound = true; + } + } + } + } + + if ( found !== undefined ) { + if ( !inplace ) { + curLoop = result; + } + + expr = expr.replace( Expr.match[ type ], "" ); + + if ( !anyFound ) { + return []; + } + + break; + } + } + } + + // Improper expression + if ( expr === old ) { + if ( anyFound == null ) { + Sizzle.error( expr ); + + } else { + break; + } + } + + old = expr; + } + + return curLoop; +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +/** + * Utility function for retreiving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +var getText = Sizzle.getText = function( elem ) { + var i, node, + nodeType = elem.nodeType, + ret = ""; + + if ( nodeType ) { + if ( nodeType === 1 || nodeType === 9 ) { + // Use textContent || innerText for elements + if ( typeof elem.textContent === 'string' ) { + return elem.textContent; + } else if ( typeof elem.innerText === 'string' ) { + // Replace IE's carriage returns + return elem.innerText.replace( rReturn, '' ); + } else { + // Traverse it's children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + } else { + + // If no nodeType, this is expected to be an array + for ( i = 0; (node = elem[i]); i++ ) { + // Do not traverse comment nodes + if ( node.nodeType !== 8 ) { + ret += getText( node ); + } + } + } + return ret; +}; + +var Expr = Sizzle.selectors = { + order: [ "ID", "NAME", "TAG" ], + + match: { + ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, + CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, + NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/, + ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/, + TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/, + CHILD: /:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/, + POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/, + PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/ + }, + + leftMatch: {}, + + attrMap: { + "class": "className", + "for": "htmlFor" + }, + + attrHandle: { + href: function( elem ) { + return elem.getAttribute( "href" ); + }, + type: function( elem ) { + return elem.getAttribute( "type" ); + } + }, + + relative: { + "+": function(checkSet, part){ + var isPartStr = typeof part === "string", + isTag = isPartStr && !rNonWord.test( part ), + isPartStrNotTag = isPartStr && !isTag; + + if ( isTag ) { + part = part.toLowerCase(); + } + + for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { + if ( (elem = checkSet[i]) ) { + while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} + + checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ? + elem || false : + elem === part; + } + } + + if ( isPartStrNotTag ) { + Sizzle.filter( part, checkSet, true ); + } + }, + + ">": function( checkSet, part ) { + var elem, + isPartStr = typeof part === "string", + i = 0, + l = checkSet.length; + + if ( isPartStr && !rNonWord.test( part ) ) { + part = part.toLowerCase(); + + for ( ; i < l; i++ ) { + elem = checkSet[i]; + + if ( elem ) { + var parent = elem.parentNode; + checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false; + } + } + + } else { + for ( ; i < l; i++ ) { + elem = checkSet[i]; + + if ( elem ) { + checkSet[i] = isPartStr ? + elem.parentNode : + elem.parentNode === part; + } + } + + if ( isPartStr ) { + Sizzle.filter( part, checkSet, true ); + } + } + }, + + "": function(checkSet, part, isXML){ + var nodeCheck, + doneName = done++, + checkFn = dirCheck; + + if ( typeof part === "string" && !rNonWord.test( part ) ) { + part = part.toLowerCase(); + nodeCheck = part; + checkFn = dirNodeCheck; + } + + checkFn( "parentNode", part, doneName, checkSet, nodeCheck, isXML ); + }, + + "~": function( checkSet, part, isXML ) { + var nodeCheck, + doneName = done++, + checkFn = dirCheck; + + if ( typeof part === "string" && !rNonWord.test( part ) ) { + part = part.toLowerCase(); + nodeCheck = part; + checkFn = dirNodeCheck; + } + + checkFn( "previousSibling", part, doneName, checkSet, nodeCheck, isXML ); + } + }, + + find: { + ID: function( match, context, isXML ) { + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + return m && m.parentNode ? [m] : []; + } + }, + + NAME: function( match, context ) { + if ( typeof context.getElementsByName !== "undefined" ) { + var ret = [], + results = context.getElementsByName( match[1] ); + + for ( var i = 0, l = results.length; i < l; i++ ) { + if ( results[i].getAttribute("name") === match[1] ) { + ret.push( results[i] ); + } + } + + return ret.length === 0 ? null : ret; + } + }, + + TAG: function( match, context ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { + return context.getElementsByTagName( match[1] ); + } + } + }, + preFilter: { + CLASS: function( match, curLoop, inplace, result, not, isXML ) { + match = " " + match[1].replace( rBackslash, "" ) + " "; + + if ( isXML ) { + return match; + } + + for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) { + if ( elem ) { + if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n\r]/g, " ").indexOf(match) >= 0) ) { + if ( !inplace ) { + result.push( elem ); + } + + } else if ( inplace ) { + curLoop[i] = false; + } + } + } + + return false; + }, + + ID: function( match ) { + return match[1].replace( rBackslash, "" ); + }, + + TAG: function( match, curLoop ) { + return match[1].replace( rBackslash, "" ).toLowerCase(); + }, + + CHILD: function( match ) { + if ( match[1] === "nth" ) { + if ( !match[2] ) { + Sizzle.error( match[0] ); + } + + match[2] = match[2].replace(/^\+|\s*/g, ''); + + // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6' + var test = /(-?)(\d*)(?:n([+\-]?\d*))?/.exec( + match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" || + !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); + + // calculate the numbers (first)n+(last) including if they are negative + match[2] = (test[1] + (test[2] || 1)) - 0; + match[3] = test[3] - 0; + } + else if ( match[2] ) { + Sizzle.error( match[0] ); + } + + // TODO: Move to normal caching system + match[0] = done++; + + return match; + }, + + ATTR: function( match, curLoop, inplace, result, not, isXML ) { + var name = match[1] = match[1].replace( rBackslash, "" ); + + if ( !isXML && Expr.attrMap[name] ) { + match[1] = Expr.attrMap[name]; + } + + // Handle if an un-quoted value was used + match[4] = ( match[4] || match[5] || "" ).replace( rBackslash, "" ); + + if ( match[2] === "~=" ) { + match[4] = " " + match[4] + " "; + } + + return match; + }, + + PSEUDO: function( match, curLoop, inplace, result, not ) { + if ( match[1] === "not" ) { + // If we're dealing with a complex expression, or a simple one + if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { + match[3] = Sizzle(match[3], null, null, curLoop); + + } else { + var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); + + if ( !inplace ) { + result.push.apply( result, ret ); + } + + return false; + } + + } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { + return true; + } + + return match; + }, + + POS: function( match ) { + match.unshift( true ); + + return match; + } + }, + + filters: { + enabled: function( elem ) { + return elem.disabled === false && elem.type !== "hidden"; + }, + + disabled: function( elem ) { + return elem.disabled === true; + }, + + checked: function( elem ) { + return elem.checked === true; + }, + + selected: function( elem ) { + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + parent: function( elem ) { + return !!elem.firstChild; + }, + + empty: function( elem ) { + return !elem.firstChild; + }, + + has: function( elem, i, match ) { + return !!Sizzle( match[3], elem ).length; + }, + + header: function( elem ) { + return (/h\d/i).test( elem.nodeName ); + }, + + text: function( elem ) { + var attr = elem.getAttribute( "type" ), type = elem.type; + // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) + // use getAttribute instead to test this case + return elem.nodeName.toLowerCase() === "input" && "text" === type && ( attr === type || attr === null ); + }, + + radio: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "radio" === elem.type; + }, + + checkbox: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "checkbox" === elem.type; + }, + + file: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "file" === elem.type; + }, + + password: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "password" === elem.type; + }, + + submit: function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && "submit" === elem.type; + }, + + image: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "image" === elem.type; + }, + + reset: function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && "reset" === elem.type; + }, + + button: function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && "button" === elem.type || name === "button"; + }, + + input: function( elem ) { + return (/input|select|textarea|button/i).test( elem.nodeName ); + }, + + focus: function( elem ) { + return elem === elem.ownerDocument.activeElement; + } + }, + setFilters: { + first: function( elem, i ) { + return i === 0; + }, + + last: function( elem, i, match, array ) { + return i === array.length - 1; + }, + + even: function( elem, i ) { + return i % 2 === 0; + }, + + odd: function( elem, i ) { + return i % 2 === 1; + }, + + lt: function( elem, i, match ) { + return i < match[3] - 0; + }, + + gt: function( elem, i, match ) { + return i > match[3] - 0; + }, + + nth: function( elem, i, match ) { + return match[3] - 0 === i; + }, + + eq: function( elem, i, match ) { + return match[3] - 0 === i; + } + }, + filter: { + PSEUDO: function( elem, match, i, array ) { + var name = match[1], + filter = Expr.filters[ name ]; + + if ( filter ) { + return filter( elem, i, match, array ); + + } else if ( name === "contains" ) { + return (elem.textContent || elem.innerText || getText([ elem ]) || "").indexOf(match[3]) >= 0; + + } else if ( name === "not" ) { + var not = match[3]; + + for ( var j = 0, l = not.length; j < l; j++ ) { + if ( not[j] === elem ) { + return false; + } + } + + return true; + + } else { + Sizzle.error( name ); + } + }, + + CHILD: function( elem, match ) { + var first, last, + doneName, parent, cache, + count, diff, + type = match[1], + node = elem; + + switch ( type ) { + case "only": + case "first": + while ( (node = node.previousSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + + if ( type === "first" ) { + return true; + } + + node = elem; + + case "last": + while ( (node = node.nextSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + + return true; + + case "nth": + first = match[2]; + last = match[3]; + + if ( first === 1 && last === 0 ) { + return true; + } + + doneName = match[0]; + parent = elem.parentNode; + + if ( parent && (parent[ expando ] !== doneName || !elem.nodeIndex) ) { + count = 0; + + for ( node = parent.firstChild; node; node = node.nextSibling ) { + if ( node.nodeType === 1 ) { + node.nodeIndex = ++count; + } + } + + parent[ expando ] = doneName; + } + + diff = elem.nodeIndex - last; + + if ( first === 0 ) { + return diff === 0; + + } else { + return ( diff % first === 0 && diff / first >= 0 ); + } + } + }, + + ID: function( elem, match ) { + return elem.nodeType === 1 && elem.getAttribute("id") === match; + }, + + TAG: function( elem, match ) { + return (match === "*" && elem.nodeType === 1) || !!elem.nodeName && elem.nodeName.toLowerCase() === match; + }, + + CLASS: function( elem, match ) { + return (" " + (elem.className || elem.getAttribute("class")) + " ") + .indexOf( match ) > -1; + }, + + ATTR: function( elem, match ) { + var name = match[1], + result = Sizzle.attr ? + Sizzle.attr( elem, name ) : + Expr.attrHandle[ name ] ? + Expr.attrHandle[ name ]( elem ) : + elem[ name ] != null ? + elem[ name ] : + elem.getAttribute( name ), + value = result + "", + type = match[2], + check = match[4]; + + return result == null ? + type === "!=" : + !type && Sizzle.attr ? + result != null : + type === "=" ? + value === check : + type === "*=" ? + value.indexOf(check) >= 0 : + type === "~=" ? + (" " + value + " ").indexOf(check) >= 0 : + !check ? + value && result !== false : + type === "!=" ? + value !== check : + type === "^=" ? + value.indexOf(check) === 0 : + type === "$=" ? + value.substr(value.length - check.length) === check : + type === "|=" ? + value === check || value.substr(0, check.length + 1) === check + "-" : + false; + }, + + POS: function( elem, match, i, array ) { + var name = match[2], + filter = Expr.setFilters[ name ]; + + if ( filter ) { + return filter( elem, i, match, array ); + } + } + } +}; + +var origPOS = Expr.match.POS, + fescape = function(all, num){ + return "\\" + (num - 0 + 1); + }; + +for ( var type in Expr.match ) { + Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) ); + Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) ); +} + +var makeArray = function( array, results ) { + array = Array.prototype.slice.call( array, 0 ); + + if ( results ) { + results.push.apply( results, array ); + return results; + } + + return array; +}; + +// Perform a simple check to determine if the browser is capable of +// converting a NodeList to an array using builtin methods. +// Also verifies that the returned array holds DOM nodes +// (which is not the case in the Blackberry browser) +try { + Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType; + +// Provide a fallback method if it does not work +} catch( e ) { + makeArray = function( array, results ) { + var i = 0, + ret = results || []; + + if ( toString.call(array) === "[object Array]" ) { + Array.prototype.push.apply( ret, array ); + + } else { + if ( typeof array.length === "number" ) { + for ( var l = array.length; i < l; i++ ) { + ret.push( array[i] ); + } + + } else { + for ( ; array[i]; i++ ) { + ret.push( array[i] ); + } + } + } + + return ret; + }; +} + +var sortOrder, siblingCheck; + +if ( document.documentElement.compareDocumentPosition ) { + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { + return a.compareDocumentPosition ? -1 : 1; + } + + return a.compareDocumentPosition(b) & 4 ? -1 : 1; + }; + +} else { + sortOrder = function( a, b ) { + // The nodes are identical, we can exit early + if ( a === b ) { + hasDuplicate = true; + return 0; + + // Fallback to using sourceIndex (in IE) if it's available on both nodes + } else if ( a.sourceIndex && b.sourceIndex ) { + return a.sourceIndex - b.sourceIndex; + } + + var al, bl, + ap = [], + bp = [], + aup = a.parentNode, + bup = b.parentNode, + cur = aup; + + // If the nodes are siblings (or identical) we can do a quick check + if ( aup === bup ) { + return siblingCheck( a, b ); + + // If no parents were found then the nodes are disconnected + } else if ( !aup ) { + return -1; + + } else if ( !bup ) { + return 1; + } + + // Otherwise they're somewhere else in the tree so we need + // to build up a full list of the parentNodes for comparison + while ( cur ) { + ap.unshift( cur ); + cur = cur.parentNode; + } + + cur = bup; + + while ( cur ) { + bp.unshift( cur ); + cur = cur.parentNode; + } + + al = ap.length; + bl = bp.length; + + // Start walking down the tree looking for a discrepancy + for ( var i = 0; i < al && i < bl; i++ ) { + if ( ap[i] !== bp[i] ) { + return siblingCheck( ap[i], bp[i] ); + } + } + + // We ended someplace up the tree so do a sibling check + return i === al ? + siblingCheck( a, bp[i], -1 ) : + siblingCheck( ap[i], b, 1 ); + }; + + siblingCheck = function( a, b, ret ) { + if ( a === b ) { + return ret; + } + + var cur = a.nextSibling; + + while ( cur ) { + if ( cur === b ) { + return -1; + } + + cur = cur.nextSibling; + } + + return 1; + }; +} + +// Check to see if the browser returns elements by name when +// querying by getElementById (and provide a workaround) +(function(){ + // We're going to inject a fake input element with a specified name + var form = document.createElement("div"), + id = "script" + (new Date()).getTime(), + root = document.documentElement; + + form.innerHTML = "<a name='" + id + "'/>"; + + // Inject it into the root element, check its status, and remove it quickly + root.insertBefore( form, root.firstChild ); + + // The workaround has to do additional checks after a getElementById + // Which slows things down for other browsers (hence the branching) + if ( document.getElementById( id ) ) { + Expr.find.ID = function( match, context, isXML ) { + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + + return m ? + m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? + [m] : + undefined : + []; + } + }; + + Expr.filter.ID = function( elem, match ) { + var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); + + return elem.nodeType === 1 && node && node.nodeValue === match; + }; + } + + root.removeChild( form ); + + // release memory in IE + root = form = null; +})(); + +(function(){ + // Check to see if the browser returns only elements + // when doing getElementsByTagName("*") + + // Create a fake element + var div = document.createElement("div"); + div.appendChild( document.createComment("") ); + + // Make sure no comments are found + if ( div.getElementsByTagName("*").length > 0 ) { + Expr.find.TAG = function( match, context ) { + var results = context.getElementsByTagName( match[1] ); + + // Filter out possible comments + if ( match[1] === "*" ) { + var tmp = []; + + for ( var i = 0; results[i]; i++ ) { + if ( results[i].nodeType === 1 ) { + tmp.push( results[i] ); + } + } + + results = tmp; + } + + return results; + }; + } + + // Check to see if an attribute returns normalized href attributes + div.innerHTML = "<a href='#'></a>"; + + if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" && + div.firstChild.getAttribute("href") !== "#" ) { + + Expr.attrHandle.href = function( elem ) { + return elem.getAttribute( "href", 2 ); + }; + } + + // release memory in IE + div = null; +})(); + +if ( document.querySelectorAll ) { + (function(){ + var oldSizzle = Sizzle, + div = document.createElement("div"), + id = "__sizzle__"; + + div.innerHTML = "<p class='TEST'></p>"; + + // Safari can't handle uppercase or unicode characters when + // in quirks mode. + if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { + return; + } + + Sizzle = function( query, context, extra, seed ) { + context = context || document; + + // Only use querySelectorAll on non-XML documents + // (ID selectors don't work in non-HTML documents) + if ( !seed && !Sizzle.isXML(context) ) { + // See if we find a selector to speed up + var match = /^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec( query ); + + if ( match && (context.nodeType === 1 || context.nodeType === 9) ) { + // Speed-up: Sizzle("TAG") + if ( match[1] ) { + return makeArray( context.getElementsByTagName( query ), extra ); + + // Speed-up: Sizzle(".CLASS") + } else if ( match[2] && Expr.find.CLASS && context.getElementsByClassName ) { + return makeArray( context.getElementsByClassName( match[2] ), extra ); + } + } + + if ( context.nodeType === 9 ) { + // Speed-up: Sizzle("body") + // The body element only exists once, optimize finding it + if ( query === "body" && context.body ) { + return makeArray( [ context.body ], extra ); + + // Speed-up: Sizzle("#ID") + } else if ( match && match[3] ) { + var elem = context.getElementById( match[3] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id === match[3] ) { + return makeArray( [ elem ], extra ); + } + + } else { + return makeArray( [], extra ); + } + } + + try { + return makeArray( context.querySelectorAll(query), extra ); + } catch(qsaError) {} + + // qSA works strangely on Element-rooted queries + // We can work around this by specifying an extra ID on the root + // and working up from there (Thanks to Andrew Dupont for the technique) + // IE 8 doesn't work on object elements + } else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { + var oldContext = context, + old = context.getAttribute( "id" ), + nid = old || id, + hasParent = context.parentNode, + relativeHierarchySelector = /^\s*[+~]/.test( query ); + + if ( !old ) { + context.setAttribute( "id", nid ); + } else { + nid = nid.replace( /'/g, "\\$&" ); + } + if ( relativeHierarchySelector && hasParent ) { + context = context.parentNode; + } + + try { + if ( !relativeHierarchySelector || hasParent ) { + return makeArray( context.querySelectorAll( "[id='" + nid + "'] " + query ), extra ); + } + + } catch(pseudoError) { + } finally { + if ( !old ) { + oldContext.removeAttribute( "id" ); + } + } + } + } + + return oldSizzle(query, context, extra, seed); + }; + + for ( var prop in oldSizzle ) { + Sizzle[ prop ] = oldSizzle[ prop ]; + } + + // release memory in IE + div = null; + })(); +} + +(function(){ + var html = document.documentElement, + matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector; + + if ( matches ) { + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9 fails this) + var disconnectedMatch = !matches.call( document.createElement( "div" ), "div" ), + pseudoWorks = false; + + try { + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( document.documentElement, "[test!='']:sizzle" ); + + } catch( pseudoError ) { + pseudoWorks = true; + } + + Sizzle.matchesSelector = function( node, expr ) { + // Make sure that attribute selectors are quoted + expr = expr.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']"); + + if ( !Sizzle.isXML( node ) ) { + try { + if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) { + var ret = matches.call( node, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || !disconnectedMatch || + // As well, disconnected nodes are said to be in a document + // fragment in IE 9, so check for that + node.document && node.document.nodeType !== 11 ) { + return ret; + } + } + } catch(e) {} + } + + return Sizzle(expr, null, null, [node]).length > 0; + }; + } +})(); + +(function(){ + var div = document.createElement("div"); + + div.innerHTML = "<div class='test e'></div><div class='test'></div>"; + + // Opera can't find a second classname (in 9.6) + // Also, make sure that getElementsByClassName actually exists + if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) { + return; + } + + // Safari caches class attributes, doesn't catch changes (in 3.2) + div.lastChild.className = "e"; + + if ( div.getElementsByClassName("e").length === 1 ) { + return; + } + + Expr.order.splice(1, 0, "CLASS"); + Expr.find.CLASS = function( match, context, isXML ) { + if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { + return context.getElementsByClassName(match[1]); + } + }; + + // release memory in IE + div = null; +})(); + +function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + + if ( elem ) { + var match = false; + + elem = elem[dir]; + + while ( elem ) { + if ( elem[ expando ] === doneName ) { + match = checkSet[elem.sizset]; + break; + } + + if ( elem.nodeType === 1 && !isXML ){ + elem[ expando ] = doneName; + elem.sizset = i; + } + + if ( elem.nodeName.toLowerCase() === cur ) { + match = elem; + break; + } + + elem = elem[dir]; + } + + checkSet[i] = match; + } + } +} + +function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + + if ( elem ) { + var match = false; + + elem = elem[dir]; + + while ( elem ) { + if ( elem[ expando ] === doneName ) { + match = checkSet[elem.sizset]; + break; + } + + if ( elem.nodeType === 1 ) { + if ( !isXML ) { + elem[ expando ] = doneName; + elem.sizset = i; + } + + if ( typeof cur !== "string" ) { + if ( elem === cur ) { + match = true; + break; + } + + } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { + match = elem; + break; + } + } + + elem = elem[dir]; + } + + checkSet[i] = match; + } + } +} + +if ( document.documentElement.contains ) { + Sizzle.contains = function( a, b ) { + return a !== b && (a.contains ? a.contains(b) : true); + }; + +} else if ( document.documentElement.compareDocumentPosition ) { + Sizzle.contains = function( a, b ) { + return !!(a.compareDocumentPosition(b) & 16); + }; + +} else { + Sizzle.contains = function() { + return false; + }; +} + +Sizzle.isXML = function( elem ) { + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement; + + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; + +var posProcess = function( selector, context, seed ) { + var match, + tmpSet = [], + later = "", + root = context.nodeType ? [context] : context; + + // Position selectors must be done after the filter + // And so must :not(positional) so we move all PSEUDOs to the end + while ( (match = Expr.match.PSEUDO.exec( selector )) ) { + later += match[0]; + selector = selector.replace( Expr.match.PSEUDO, "" ); + } + + selector = Expr.relative[selector] ? selector + "*" : selector; + + for ( var i = 0, l = root.length; i < l; i++ ) { + Sizzle( selector, root[i], tmpSet, seed ); + } + + return Sizzle.filter( later, tmpSet ); +}; + +// EXPOSE +// Override sizzle attribute retrieval +Sizzle.attr = jQuery.attr; +Sizzle.selectors.attrMap = {}; +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; +jQuery.expr[":"] = jQuery.expr.filters; +jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; + + +})(); + + +var runtil = /Until$/, + rparentsprev = /^(?:parents|prevUntil|prevAll)/, + // Note: This RegExp should be improved, or likely pulled from Sizzle + rmultiselector = /,/, + isSimple = /^.[^:#\[\.,]*$/, + slice = Array.prototype.slice, + POS = jQuery.expr.match.POS, + // methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend({ + find: function( selector ) { + var self = this, + i, l; + + if ( typeof selector !== "string" ) { + return jQuery( selector ).filter(function() { + for ( i = 0, l = self.length; i < l; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + }); + } + + var ret = this.pushStack( "", "find", selector ), + length, n, r; + + for ( i = 0, l = this.length; i < l; i++ ) { + length = ret.length; + jQuery.find( selector, this[i], ret ); + + if ( i > 0 ) { + // Make sure that the results are unique + for ( n = length; n < ret.length; n++ ) { + for ( r = 0; r < length; r++ ) { + if ( ret[r] === ret[n] ) { + ret.splice(n--, 1); + break; + } + } + } + } + } + + return ret; + }, + + has: function( target ) { + var targets = jQuery( target ); + return this.filter(function() { + for ( var i = 0, l = targets.length; i < l; i++ ) { + if ( jQuery.contains( this, targets[i] ) ) { + return true; + } + } + }); + }, + + not: function( selector ) { + return this.pushStack( winnow(this, selector, false), "not", selector); + }, + + filter: function( selector ) { + return this.pushStack( winnow(this, selector, true), "filter", selector ); + }, + + is: function( selector ) { + return !!selector && ( + typeof selector === "string" ? + // If this is a positional selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + POS.test( selector ) ? + jQuery( selector, this.context ).index( this[0] ) >= 0 : + jQuery.filter( selector, this ).length > 0 : + this.filter( selector ).length > 0 ); + }, + + closest: function( selectors, context ) { + var ret = [], i, l, cur = this[0]; + + // Array (deprecated as of jQuery 1.7) + if ( jQuery.isArray( selectors ) ) { + var level = 1; + + while ( cur && cur.ownerDocument && cur !== context ) { + for ( i = 0; i < selectors.length; i++ ) { + + if ( jQuery( cur ).is( selectors[ i ] ) ) { + ret.push({ selector: selectors[ i ], elem: cur, level: level }); + } + } + + cur = cur.parentNode; + level++; + } + + return ret; + } + + // String + var pos = POS.test( selectors ) || typeof selectors !== "string" ? + jQuery( selectors, context || this.context ) : + 0; + + for ( i = 0, l = this.length; i < l; i++ ) { + cur = this[i]; + + while ( cur ) { + if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) { + ret.push( cur ); + break; + + } else { + cur = cur.parentNode; + if ( !cur || !cur.ownerDocument || cur === context || cur.nodeType === 11 ) { + break; + } + } + } + } + + ret = ret.length > 1 ? jQuery.unique( ret ) : ret; + + return this.pushStack( ret, "closest", selectors ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[0] && this[0].parentNode ) ? this.prevAll().length : -1; + } + + // index in selector + if ( typeof elem === "string" ) { + return jQuery.inArray( this[0], jQuery( elem ) ); + } + + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[0] : elem, this ); + }, + + add: function( selector, context ) { + var set = typeof selector === "string" ? + jQuery( selector, context ) : + jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ), + all = jQuery.merge( this.get(), set ); + + return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ? + all : + jQuery.unique( all ) ); + }, + + andSelf: function() { + return this.add( this.prevObject ); + } +}); + +// A painfully simple check to see if an element is disconnected +// from a document (should be improved, where feasible). +function isDisconnected( node ) { + return !node || !node.parentNode || node.parentNode.nodeType === 11; +} + +jQuery.each({ + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return jQuery.dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return jQuery.dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return jQuery.nth( elem, 2, "nextSibling" ); + }, + prev: function( elem ) { + return jQuery.nth( elem, 2, "previousSibling" ); + }, + nextAll: function( elem ) { + return jQuery.dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return jQuery.dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return jQuery.dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return jQuery.dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return jQuery.sibling( elem.parentNode.firstChild, elem ); + }, + children: function( elem ) { + return jQuery.sibling( elem.firstChild ); + }, + contents: function( elem ) { + return jQuery.nodeName( elem, "iframe" ) ? + elem.contentDocument || elem.contentWindow.document : + jQuery.makeArray( elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var ret = jQuery.map( this, fn, until ); + + if ( !runtil.test( name ) ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + ret = jQuery.filter( selector, ret ); + } + + ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret; + + if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) { + ret = ret.reverse(); + } + + return this.pushStack( ret, name, slice.call( arguments ).join(",") ); + }; +}); + +jQuery.extend({ + filter: function( expr, elems, not ) { + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 ? + jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] : + jQuery.find.matches(expr, elems); + }, + + dir: function( elem, dir, until ) { + var matched = [], + cur = elem[ dir ]; + + while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { + if ( cur.nodeType === 1 ) { + matched.push( cur ); + } + cur = cur[dir]; + } + return matched; + }, + + nth: function( cur, result, dir, elem ) { + result = result || 1; + var num = 0; + + for ( ; cur; cur = cur[dir] ) { + if ( cur.nodeType === 1 && ++num === result ) { + break; + } + } + + return cur; + }, + + sibling: function( n, elem ) { + var r = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + r.push( n ); + } + } + + return r; + } +}); + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, keep ) { + + // Can't pass null or undefined to indexOf in Firefox 4 + // Set to 0 to skip string check + qualifier = qualifier || 0; + + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep(elements, function( elem, i ) { + var retVal = !!qualifier.call( elem, i, elem ); + return retVal === keep; + }); + + } else if ( qualifier.nodeType ) { + return jQuery.grep(elements, function( elem, i ) { + return ( elem === qualifier ) === keep; + }); + + } else if ( typeof qualifier === "string" ) { + var filtered = jQuery.grep(elements, function( elem ) { + return elem.nodeType === 1; + }); + + if ( isSimple.test( qualifier ) ) { + return jQuery.filter(qualifier, filtered, !keep); + } else { + qualifier = jQuery.filter( qualifier, filtered ); + } + } + + return jQuery.grep(elements, function( elem, i ) { + return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep; + }); +} + + + + +function createSafeFragment( document ) { + var list = nodeNames.split( "|" ), + safeFrag = document.createDocumentFragment(); + + if ( safeFrag.createElement ) { + while ( list.length ) { + safeFrag.createElement( + list.pop() + ); + } + } + return safeFrag; +} + +var nodeNames = "abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|" + + "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", + rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, + rleadingWhitespace = /^\s+/, + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig, + rtagName = /<([\w:]+)/, + rtbody = /<tbody/i, + rhtml = /<|&#?\w+;/, + rnoInnerhtml = /<(?:script|style)/i, + rnocache = /<(?:script|object|embed|option|style)/i, + rnoshimcache = new RegExp("<(?:" + nodeNames + ")", "i"), + // checked="checked" or checked + rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, + rscriptType = /\/(java|ecma)script/i, + rcleanScript = /^\s*<!(?:\[CDATA\[|\-\-)/, + wrapMap = { + option: [ 1, "<select multiple='multiple'>", "</select>" ], + legend: [ 1, "<fieldset>", "</fieldset>" ], + thead: [ 1, "<table>", "</table>" ], + tr: [ 2, "<table><tbody>", "</tbody></table>" ], + td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ], + col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ], + area: [ 1, "<map>", "</map>" ], + _default: [ 0, "", "" ] + }, + safeFragment = createSafeFragment( document ); + +wrapMap.optgroup = wrapMap.option; +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +// IE can't serialize <link> and <script> tags normally +if ( !jQuery.support.htmlSerialize ) { + wrapMap._default = [ 1, "div<div>", "</div>" ]; +} + +jQuery.fn.extend({ + text: function( text ) { + if ( jQuery.isFunction(text) ) { + return this.each(function(i) { + var self = jQuery( this ); + + self.text( text.call(this, i, self.text()) ); + }); + } + + if ( typeof text !== "object" && text !== undefined ) { + return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) ); + } + + return jQuery.text( this ); + }, + + wrapAll: function( html ) { + if ( jQuery.isFunction( html ) ) { + return this.each(function(i) { + jQuery(this).wrapAll( html.call(this, i) ); + }); + } + + if ( this[0] ) { + // The elements to wrap the target around + var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true); + + if ( this[0].parentNode ) { + wrap.insertBefore( this[0] ); + } + + wrap.map(function() { + var elem = this; + + while ( elem.firstChild && elem.firstChild.nodeType === 1 ) { + elem = elem.firstChild; + } + + return elem; + }).append( this ); + } + + return this; + }, + + wrapInner: function( html ) { + if ( jQuery.isFunction( html ) ) { + return this.each(function(i) { + jQuery(this).wrapInner( html.call(this, i) ); + }); + } + + return this.each(function() { + var self = jQuery( this ), + contents = self.contents(); + + if ( contents.length ) { + contents.wrapAll( html ); + + } else { + self.append( html ); + } + }); + }, + + wrap: function( html ) { + var isFunction = jQuery.isFunction( html ); + + return this.each(function(i) { + jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html ); + }); + }, + + unwrap: function() { + return this.parent().each(function() { + if ( !jQuery.nodeName( this, "body" ) ) { + jQuery( this ).replaceWith( this.childNodes ); + } + }).end(); + }, + + append: function() { + return this.domManip(arguments, true, function( elem ) { + if ( this.nodeType === 1 ) { + this.appendChild( elem ); + } + }); + }, + + prepend: function() { + return this.domManip(arguments, true, function( elem ) { + if ( this.nodeType === 1 ) { + this.insertBefore( elem, this.firstChild ); + } + }); + }, + + before: function() { + if ( this[0] && this[0].parentNode ) { + return this.domManip(arguments, false, function( elem ) { + this.parentNode.insertBefore( elem, this ); + }); + } else if ( arguments.length ) { + var set = jQuery.clean( arguments ); + set.push.apply( set, this.toArray() ); + return this.pushStack( set, "before", arguments ); + } + }, + + after: function() { + if ( this[0] && this[0].parentNode ) { + return this.domManip(arguments, false, function( elem ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + }); + } else if ( arguments.length ) { + var set = this.pushStack( this, "after", arguments ); + set.push.apply( set, jQuery.clean(arguments) ); + return set; + } + }, + + // keepData is for internal use only--do not document + remove: function( selector, keepData ) { + for ( var i = 0, elem; (elem = this[i]) != null; i++ ) { + if ( !selector || jQuery.filter( selector, [ elem ] ).length ) { + if ( !keepData && elem.nodeType === 1 ) { + jQuery.cleanData( elem.getElementsByTagName("*") ); + jQuery.cleanData( [ elem ] ); + } + + if ( elem.parentNode ) { + elem.parentNode.removeChild( elem ); + } + } + } + + return this; + }, + + empty: function() { + for ( var i = 0, elem; (elem = this[i]) != null; i++ ) { + // Remove element nodes and prevent memory leaks + if ( elem.nodeType === 1 ) { + jQuery.cleanData( elem.getElementsByTagName("*") ); + } + + // Remove any remaining nodes + while ( elem.firstChild ) { + elem.removeChild( elem.firstChild ); + } + } + + return this; + }, + + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; + + return this.map( function () { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + }); + }, + + html: function( value ) { + if ( value === undefined ) { + return this[0] && this[0].nodeType === 1 ? + this[0].innerHTML.replace(rinlinejQuery, "") : + null; + + // See if we can take a shortcut and just use innerHTML + } else if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + (jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value )) && + !wrapMap[ (rtagName.exec( value ) || ["", ""])[1].toLowerCase() ] ) { + + value = value.replace(rxhtmlTag, "<$1></$2>"); + + try { + for ( var i = 0, l = this.length; i < l; i++ ) { + // Remove element nodes and prevent memory leaks + if ( this[i].nodeType === 1 ) { + jQuery.cleanData( this[i].getElementsByTagName("*") ); + this[i].innerHTML = value; + } + } + + // If using innerHTML throws an exception, use the fallback method + } catch(e) { + this.empty().append( value ); + } + + } else if ( jQuery.isFunction( value ) ) { + this.each(function(i){ + var self = jQuery( this ); + + self.html( value.call(this, i, self.html()) ); + }); + + } else { + this.empty().append( value ); + } + + return this; + }, + + replaceWith: function( value ) { + if ( this[0] && this[0].parentNode ) { + // Make sure that the elements are removed from the DOM before they are inserted + // this can help fix replacing a parent with child elements + if ( jQuery.isFunction( value ) ) { + return this.each(function(i) { + var self = jQuery(this), old = self.html(); + self.replaceWith( value.call( this, i, old ) ); + }); + } + + if ( typeof value !== "string" ) { + value = jQuery( value ).detach(); + } + + return this.each(function() { + var next = this.nextSibling, + parent = this.parentNode; + + jQuery( this ).remove(); + + if ( next ) { + jQuery(next).before( value ); + } else { + jQuery(parent).append( value ); + } + }); + } else { + return this.length ? + this.pushStack( jQuery(jQuery.isFunction(value) ? value() : value), "replaceWith", value ) : + this; + } + }, + + detach: function( selector ) { + return this.remove( selector, true ); + }, + + domManip: function( args, table, callback ) { + var results, first, fragment, parent, + value = args[0], + scripts = []; + + // We can't cloneNode fragments that contain checked, in WebKit + if ( !jQuery.support.checkClone && arguments.length === 3 && typeof value === "string" && rchecked.test( value ) ) { + return this.each(function() { + jQuery(this).domManip( args, table, callback, true ); + }); + } + + if ( jQuery.isFunction(value) ) { + return this.each(function(i) { + var self = jQuery(this); + args[0] = value.call(this, i, table ? self.html() : undefined); + self.domManip( args, table, callback ); + }); + } + + if ( this[0] ) { + parent = value && value.parentNode; + + // If we're in a fragment, just use that instead of building a new one + if ( jQuery.support.parentNode && parent && parent.nodeType === 11 && parent.childNodes.length === this.length ) { + results = { fragment: parent }; + + } else { + results = jQuery.buildFragment( args, this, scripts ); + } + + fragment = results.fragment; + + if ( fragment.childNodes.length === 1 ) { + first = fragment = fragment.firstChild; + } else { + first = fragment.firstChild; + } + + if ( first ) { + table = table && jQuery.nodeName( first, "tr" ); + + for ( var i = 0, l = this.length, lastIndex = l - 1; i < l; i++ ) { + callback.call( + table ? + root(this[i], first) : + this[i], + // Make sure that we do not leak memory by inadvertently discarding + // the original fragment (which might have attached data) instead of + // using it; in addition, use the original fragment object for the last + // item instead of first because it can end up being emptied incorrectly + // in certain situations (Bug #8070). + // Fragments from the fragment cache must always be cloned and never used + // in place. + results.cacheable || ( l > 1 && i < lastIndex ) ? + jQuery.clone( fragment, true, true ) : + fragment + ); + } + } + + if ( scripts.length ) { + jQuery.each( scripts, evalScript ); + } + } + + return this; + } +}); + +function root( elem, cur ) { + return jQuery.nodeName(elem, "table") ? + (elem.getElementsByTagName("tbody")[0] || + elem.appendChild(elem.ownerDocument.createElement("tbody"))) : + elem; +} + +function cloneCopyEvent( src, dest ) { + + if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) { + return; + } + + var type, i, l, + oldData = jQuery._data( src ), + curData = jQuery._data( dest, oldData ), + events = oldData.events; + + if ( events ) { + delete curData.handle; + curData.events = {}; + + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type + ( events[ type ][ i ].namespace ? "." : "" ) + events[ type ][ i ].namespace, events[ type ][ i ], events[ type ][ i ].data ); + } + } + } + + // make the cloned public data object a copy from the original + if ( curData.data ) { + curData.data = jQuery.extend( {}, curData.data ); + } +} + +function cloneFixAttributes( src, dest ) { + var nodeName; + + // We do not need to do anything for non-Elements + if ( dest.nodeType !== 1 ) { + return; + } + + // clearAttributes removes the attributes, which we don't want, + // but also removes the attachEvent events, which we *do* want + if ( dest.clearAttributes ) { + dest.clearAttributes(); + } + + // mergeAttributes, in contrast, only merges back on the + // original attributes, not the events + if ( dest.mergeAttributes ) { + dest.mergeAttributes( src ); + } + + nodeName = dest.nodeName.toLowerCase(); + + // IE6-8 fail to clone children inside object elements that use + // the proprietary classid attribute value (rather than the type + // attribute) to identify the type of content to display + if ( nodeName === "object" ) { + dest.outerHTML = src.outerHTML; + + } else if ( nodeName === "input" && (src.type === "checkbox" || src.type === "radio") ) { + // IE6-8 fails to persist the checked state of a cloned checkbox + // or radio button. Worse, IE6-7 fail to give the cloned element + // a checked appearance if the defaultChecked value isn't also set + if ( src.checked ) { + dest.defaultChecked = dest.checked = src.checked; + } + + // IE6-7 get confused and end up setting the value of a cloned + // checkbox/radio button to an empty string instead of "on" + if ( dest.value !== src.value ) { + dest.value = src.value; + } + + // IE6-8 fails to return the selected option to the default selected + // state when cloning options + } else if ( nodeName === "option" ) { + dest.selected = src.defaultSelected; + + // IE6-8 fails to set the defaultValue to the correct value when + // cloning other types of input fields + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + } + + // Event data gets referenced instead of copied if the expando + // gets copied too + dest.removeAttribute( jQuery.expando ); +} + +jQuery.buildFragment = function( args, nodes, scripts ) { + var fragment, cacheable, cacheresults, doc, + first = args[ 0 ]; + + // nodes may contain either an explicit document object, + // a jQuery collection or context object. + // If nodes[0] contains a valid object to assign to doc + if ( nodes && nodes[0] ) { + doc = nodes[0].ownerDocument || nodes[0]; + } + + // Ensure that an attr object doesn't incorrectly stand in as a document object + // Chrome and Firefox seem to allow this to occur and will throw exception + // Fixes #8950 + if ( !doc.createDocumentFragment ) { + doc = document; + } + + // Only cache "small" (1/2 KB) HTML strings that are associated with the main document + // Cloning options loses the selected state, so don't cache them + // IE 6 doesn't like it when you put <object> or <embed> elements in a fragment + // Also, WebKit does not clone 'checked' attributes on cloneNode, so don't cache + // Lastly, IE6,7,8 will not correctly reuse cached fragments that were created from unknown elems #10501 + if ( args.length === 1 && typeof first === "string" && first.length < 512 && doc === document && + first.charAt(0) === "<" && !rnocache.test( first ) && + (jQuery.support.checkClone || !rchecked.test( first )) && + (jQuery.support.html5Clone || !rnoshimcache.test( first )) ) { + + cacheable = true; + + cacheresults = jQuery.fragments[ first ]; + if ( cacheresults && cacheresults !== 1 ) { + fragment = cacheresults; + } + } + + if ( !fragment ) { + fragment = doc.createDocumentFragment(); + jQuery.clean( args, doc, fragment, scripts ); + } + + if ( cacheable ) { + jQuery.fragments[ first ] = cacheresults ? fragment : 1; + } + + return { fragment: fragment, cacheable: cacheable }; +}; + +jQuery.fragments = {}; + +jQuery.each({ + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var ret = [], + insert = jQuery( selector ), + parent = this.length === 1 && this[0].parentNode; + + if ( parent && parent.nodeType === 11 && parent.childNodes.length === 1 && insert.length === 1 ) { + insert[ original ]( this[0] ); + return this; + + } else { + for ( var i = 0, l = insert.length; i < l; i++ ) { + var elems = ( i > 0 ? this.clone(true) : this ).get(); + jQuery( insert[i] )[ original ]( elems ); + ret = ret.concat( elems ); + } + + return this.pushStack( ret, name, insert.selector ); + } + }; +}); + +function getAll( elem ) { + if ( typeof elem.getElementsByTagName !== "undefined" ) { + return elem.getElementsByTagName( "*" ); + + } else if ( typeof elem.querySelectorAll !== "undefined" ) { + return elem.querySelectorAll( "*" ); + + } else { + return []; + } +} + +// Used in clean, fixes the defaultChecked property +function fixDefaultChecked( elem ) { + if ( elem.type === "checkbox" || elem.type === "radio" ) { + elem.defaultChecked = elem.checked; + } +} +// Finds all inputs and passes them to fixDefaultChecked +function findInputs( elem ) { + var nodeName = ( elem.nodeName || "" ).toLowerCase(); + if ( nodeName === "input" ) { + fixDefaultChecked( elem ); + // Skip scripts, get other children + } else if ( nodeName !== "script" && typeof elem.getElementsByTagName !== "undefined" ) { + jQuery.grep( elem.getElementsByTagName("input"), fixDefaultChecked ); + } +} + +// Derived From: http://www.iecss.com/shimprove/javascript/shimprove.1-0-1.js +function shimCloneNode( elem ) { + var div = document.createElement( "div" ); + safeFragment.appendChild( div ); + + div.innerHTML = elem.outerHTML; + return div.firstChild; +} + +jQuery.extend({ + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var srcElements, + destElements, + i, + // IE<=8 does not properly clone detached, unknown element nodes + clone = jQuery.support.html5Clone || !rnoshimcache.test( "<" + elem.nodeName ) ? + elem.cloneNode( true ) : + shimCloneNode( elem ); + + if ( (!jQuery.support.noCloneEvent || !jQuery.support.noCloneChecked) && + (elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) { + // IE copies events bound via attachEvent when using cloneNode. + // Calling detachEvent on the clone will also remove the events + // from the original. In order to get around this, we use some + // proprietary methods to clear the events. Thanks to MooTools + // guys for this hotness. + + cloneFixAttributes( elem, clone ); + + // Using Sizzle here is crazy slow, so we use getElementsByTagName instead + srcElements = getAll( elem ); + destElements = getAll( clone ); + + // Weird iteration because IE will replace the length property + // with an element if you are cloning the body and one of the + // elements on the page has a name or id of "length" + for ( i = 0; srcElements[i]; ++i ) { + // Ensure that the destination node is not null; Fixes #9587 + if ( destElements[i] ) { + cloneFixAttributes( srcElements[i], destElements[i] ); + } + } + } + + // Copy the events from the original to the clone + if ( dataAndEvents ) { + cloneCopyEvent( elem, clone ); + + if ( deepDataAndEvents ) { + srcElements = getAll( elem ); + destElements = getAll( clone ); + + for ( i = 0; srcElements[i]; ++i ) { + cloneCopyEvent( srcElements[i], destElements[i] ); + } + } + } + + srcElements = destElements = null; + + // Return the cloned set + return clone; + }, + + clean: function( elems, context, fragment, scripts ) { + var checkScriptType; + + context = context || document; + + // !context.createElement fails in IE with an error but returns typeof 'object' + if ( typeof context.createElement === "undefined" ) { + context = context.ownerDocument || context[0] && context[0].ownerDocument || document; + } + + var ret = [], j; + + for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { + if ( typeof elem === "number" ) { + elem += ""; + } + + if ( !elem ) { + continue; + } + + // Convert html string into DOM nodes + if ( typeof elem === "string" ) { + if ( !rhtml.test( elem ) ) { + elem = context.createTextNode( elem ); + } else { + // Fix "XHTML"-style tags in all browsers + elem = elem.replace(rxhtmlTag, "<$1></$2>"); + + // Trim whitespace, otherwise indexOf won't work as expected + var tag = ( rtagName.exec( elem ) || ["", ""] )[1].toLowerCase(), + wrap = wrapMap[ tag ] || wrapMap._default, + depth = wrap[0], + div = context.createElement("div"); + + // Append wrapper element to unknown element safe doc fragment + if ( context === document ) { + // Use the fragment we've already created for this document + safeFragment.appendChild( div ); + } else { + // Use a fragment created with the owner document + createSafeFragment( context ).appendChild( div ); + } + + // Go to html and back, then peel off extra wrappers + div.innerHTML = wrap[1] + elem + wrap[2]; + + // Move to the right depth + while ( depth-- ) { + div = div.lastChild; + } + + // Remove IE's autoinserted <tbody> from table fragments + if ( !jQuery.support.tbody ) { + + // String was a <table>, *may* have spurious <tbody> + var hasBody = rtbody.test(elem), + tbody = tag === "table" && !hasBody ? + div.firstChild && div.firstChild.childNodes : + + // String was a bare <thead> or <tfoot> + wrap[1] === "<table>" && !hasBody ? + div.childNodes : + []; + + for ( j = tbody.length - 1; j >= 0 ; --j ) { + if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length ) { + tbody[ j ].parentNode.removeChild( tbody[ j ] ); + } + } + } + + // IE completely kills leading whitespace when innerHTML is used + if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) { + div.insertBefore( context.createTextNode( rleadingWhitespace.exec(elem)[0] ), div.firstChild ); + } + + elem = div.childNodes; + } + } + + // Resets defaultChecked for any radios and checkboxes + // about to be appended to the DOM in IE 6/7 (#8060) + var len; + if ( !jQuery.support.appendChecked ) { + if ( elem[0] && typeof (len = elem.length) === "number" ) { + for ( j = 0; j < len; j++ ) { + findInputs( elem[j] ); + } + } else { + findInputs( elem ); + } + } + + if ( elem.nodeType ) { + ret.push( elem ); + } else { + ret = jQuery.merge( ret, elem ); + } + } + + if ( fragment ) { + checkScriptType = function( elem ) { + return !elem.type || rscriptType.test( elem.type ); + }; + for ( i = 0; ret[i]; i++ ) { + if ( scripts && jQuery.nodeName( ret[i], "script" ) && (!ret[i].type || ret[i].type.toLowerCase() === "text/javascript") ) { + scripts.push( ret[i].parentNode ? ret[i].parentNode.removeChild( ret[i] ) : ret[i] ); + + } else { + if ( ret[i].nodeType === 1 ) { + var jsTags = jQuery.grep( ret[i].getElementsByTagName( "script" ), checkScriptType ); + + ret.splice.apply( ret, [i + 1, 0].concat( jsTags ) ); + } + fragment.appendChild( ret[i] ); + } + } + } + + return ret; + }, + + cleanData: function( elems ) { + var data, id, + cache = jQuery.cache, + special = jQuery.event.special, + deleteExpando = jQuery.support.deleteExpando; + + for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { + if ( elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()] ) { + continue; + } + + id = elem[ jQuery.expando ]; + + if ( id ) { + data = cache[ id ]; + + if ( data && data.events ) { + for ( var type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + // This is a shortcut to avoid jQuery.event.remove's overhead + } else { + jQuery.removeEvent( elem, type, data.handle ); + } + } + + // Null the DOM reference to avoid IE6/7/8 leak (#7054) + if ( data.handle ) { + data.handle.elem = null; + } + } + + if ( deleteExpando ) { + delete elem[ jQuery.expando ]; + + } else if ( elem.removeAttribute ) { + elem.removeAttribute( jQuery.expando ); + } + + delete cache[ id ]; + } + } + } +}); + +function evalScript( i, elem ) { + if ( elem.src ) { + jQuery.ajax({ + url: elem.src, + async: false, + dataType: "script" + }); + } else { + jQuery.globalEval( ( elem.text || elem.textContent || elem.innerHTML || "" ).replace( rcleanScript, "/*$0*/" ) ); + } + + if ( elem.parentNode ) { + elem.parentNode.removeChild( elem ); + } +} + + + + +var ralpha = /alpha\([^)]*\)/i, + ropacity = /opacity=([^)]*)/, + // fixed for IE9, see #8346 + rupper = /([A-Z]|^ms)/g, + rnumpx = /^-?\d+(?:px)?$/i, + rnum = /^-?\d/, + rrelNum = /^([\-+])=([\-+.\de]+)/, + + cssShow = { position: "absolute", visibility: "hidden", display: "block" }, + cssWidth = [ "Left", "Right" ], + cssHeight = [ "Top", "Bottom" ], + curCSS, + + getComputedStyle, + currentStyle; + +jQuery.fn.css = function( name, value ) { + // Setting 'undefined' is a no-op + if ( arguments.length === 2 && value === undefined ) { + return this; + } + + return jQuery.access( this, name, value, true, function( elem, name, value ) { + return value !== undefined ? + jQuery.style( elem, name, value ) : + jQuery.css( elem, name ); + }); +}; + +jQuery.extend({ + // Add in style property hooks for overriding the default + // behavior of getting and setting a style property + cssHooks: { + opacity: { + get: function( elem, computed ) { + if ( computed ) { + // We should always get a number back from opacity + var ret = curCSS( elem, "opacity", "opacity" ); + return ret === "" ? "1" : ret; + + } else { + return elem.style.opacity; + } + } + } + }, + + // Exclude the following css properties to add px + cssNumber: { + "fillOpacity": true, + "fontWeight": true, + "lineHeight": true, + "opacity": true, + "orphans": true, + "widows": true, + "zIndex": true, + "zoom": true + }, + + // Add in properties whose names you wish to fix before + // setting or getting the value + cssProps: { + // normalize float css property + "float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat" + }, + + // Get and set the style property on a DOM Node + style: function( elem, name, value, extra ) { + // Don't set styles on text and comment nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { + return; + } + + // Make sure that we're working with the right name + var ret, type, origName = jQuery.camelCase( name ), + style = elem.style, hooks = jQuery.cssHooks[ origName ]; + + name = jQuery.cssProps[ origName ] || origName; + + // Check if we're setting a value + if ( value !== undefined ) { + type = typeof value; + + // convert relative number strings (+= or -=) to relative numbers. #7345 + if ( type === "string" && (ret = rrelNum.exec( value )) ) { + value = ( +( ret[1] + 1) * +ret[2] ) + parseFloat( jQuery.css( elem, name ) ); + // Fixes bug #9237 + type = "number"; + } + + // Make sure that NaN and null values aren't set. See: #7116 + if ( value == null || type === "number" && isNaN( value ) ) { + return; + } + + // If a number was passed in, add 'px' to the (except for certain CSS properties) + if ( type === "number" && !jQuery.cssNumber[ origName ] ) { + value += "px"; + } + + // If a hook was provided, use that value, otherwise just set the specified value + if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value )) !== undefined ) { + // Wrapped to prevent IE from throwing errors when 'invalid' values are provided + // Fixes bug #5509 + try { + style[ name ] = value; + } catch(e) {} + } + + } else { + // If a hook was provided get the non-computed value from there + if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) { + return ret; + } + + // Otherwise just get the value from the style object + return style[ name ]; + } + }, + + css: function( elem, name, extra ) { + var ret, hooks; + + // Make sure that we're working with the right name + name = jQuery.camelCase( name ); + hooks = jQuery.cssHooks[ name ]; + name = jQuery.cssProps[ name ] || name; + + // cssFloat needs a special treatment + if ( name === "cssFloat" ) { + name = "float"; + } + + // If a hook was provided get the computed value from there + if ( hooks && "get" in hooks && (ret = hooks.get( elem, true, extra )) !== undefined ) { + return ret; + + // Otherwise, if a way to get the computed value exists, use that + } else if ( curCSS ) { + return curCSS( elem, name ); + } + }, + + // A method for quickly swapping in/out CSS properties to get correct calculations + swap: function( elem, options, callback ) { + var old = {}; + + // Remember the old values, and insert the new ones + for ( var name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + callback.call( elem ); + + // Revert the old values + for ( name in options ) { + elem.style[ name ] = old[ name ]; + } + } +}); + +// DEPRECATED, Use jQuery.css() instead +jQuery.curCSS = jQuery.css; + +jQuery.each(["height", "width"], function( i, name ) { + jQuery.cssHooks[ name ] = { + get: function( elem, computed, extra ) { + var val; + + if ( computed ) { + if ( elem.offsetWidth !== 0 ) { + return getWH( elem, name, extra ); + } else { + jQuery.swap( elem, cssShow, function() { + val = getWH( elem, name, extra ); + }); + } + + return val; + } + }, + + set: function( elem, value ) { + if ( rnumpx.test( value ) ) { + // ignore negative width and height values #1599 + value = parseFloat( value ); + + if ( value >= 0 ) { + return value + "px"; + } + + } else { + return value; + } + } + }; +}); + +if ( !jQuery.support.opacity ) { + jQuery.cssHooks.opacity = { + get: function( elem, computed ) { + // IE uses filters for opacity + return ropacity.test( (computed && elem.currentStyle ? elem.currentStyle.filter : elem.style.filter) || "" ) ? + ( parseFloat( RegExp.$1 ) / 100 ) + "" : + computed ? "1" : ""; + }, + + set: function( elem, value ) { + var style = elem.style, + currentStyle = elem.currentStyle, + opacity = jQuery.isNumeric( value ) ? "alpha(opacity=" + value * 100 + ")" : "", + filter = currentStyle && currentStyle.filter || style.filter || ""; + + // IE has trouble with opacity if it does not have layout + // Force it by setting the zoom level + style.zoom = 1; + + // if setting opacity to 1, and no other filters exist - attempt to remove filter attribute #6652 + if ( value >= 1 && jQuery.trim( filter.replace( ralpha, "" ) ) === "" ) { + + // Setting style.filter to null, "" & " " still leave "filter:" in the cssText + // if "filter:" is present at all, clearType is disabled, we want to avoid this + // style.removeAttribute is IE Only, but so apparently is this code path... + style.removeAttribute( "filter" ); + + // if there there is no filter style applied in a css rule, we are done + if ( currentStyle && !currentStyle.filter ) { + return; + } + } + + // otherwise, set new filter values + style.filter = ralpha.test( filter ) ? + filter.replace( ralpha, opacity ) : + filter + " " + opacity; + } + }; +} + +jQuery(function() { + // This hook cannot be added until DOM ready because the support test + // for it is not run until after DOM ready + if ( !jQuery.support.reliableMarginRight ) { + jQuery.cssHooks.marginRight = { + get: function( elem, computed ) { + // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right + // Work around by temporarily setting element display to inline-block + var ret; + jQuery.swap( elem, { "display": "inline-block" }, function() { + if ( computed ) { + ret = curCSS( elem, "margin-right", "marginRight" ); + } else { + ret = elem.style.marginRight; + } + }); + return ret; + } + }; + } +}); + +if ( document.defaultView && document.defaultView.getComputedStyle ) { + getComputedStyle = function( elem, name ) { + var ret, defaultView, computedStyle; + + name = name.replace( rupper, "-$1" ).toLowerCase(); + + if ( (defaultView = elem.ownerDocument.defaultView) && + (computedStyle = defaultView.getComputedStyle( elem, null )) ) { + ret = computedStyle.getPropertyValue( name ); + if ( ret === "" && !jQuery.contains( elem.ownerDocument.documentElement, elem ) ) { + ret = jQuery.style( elem, name ); + } + } + + return ret; + }; +} + +if ( document.documentElement.currentStyle ) { + currentStyle = function( elem, name ) { + var left, rsLeft, uncomputed, + ret = elem.currentStyle && elem.currentStyle[ name ], + style = elem.style; + + // Avoid setting ret to empty string here + // so we don't default to auto + if ( ret === null && style && (uncomputed = style[ name ]) ) { + ret = uncomputed; + } + + // From the awesome hack by Dean Edwards + // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 + + // If we're not dealing with a regular pixel number + // but a number that has a weird ending, we need to convert it to pixels + if ( !rnumpx.test( ret ) && rnum.test( ret ) ) { + + // Remember the original values + left = style.left; + rsLeft = elem.runtimeStyle && elem.runtimeStyle.left; + + // Put in the new values to get a computed value out + if ( rsLeft ) { + elem.runtimeStyle.left = elem.currentStyle.left; + } + style.left = name === "fontSize" ? "1em" : ( ret || 0 ); + ret = style.pixelLeft + "px"; + + // Revert the changed values + style.left = left; + if ( rsLeft ) { + elem.runtimeStyle.left = rsLeft; + } + } + + return ret === "" ? "auto" : ret; + }; +} + +curCSS = getComputedStyle || currentStyle; + +function getWH( elem, name, extra ) { + + // Start with offset property + var val = name === "width" ? elem.offsetWidth : elem.offsetHeight, + which = name === "width" ? cssWidth : cssHeight, + i = 0, + len = which.length; + + if ( val > 0 ) { + if ( extra !== "border" ) { + for ( ; i < len; i++ ) { + if ( !extra ) { + val -= parseFloat( jQuery.css( elem, "padding" + which[ i ] ) ) || 0; + } + if ( extra === "margin" ) { + val += parseFloat( jQuery.css( elem, extra + which[ i ] ) ) || 0; + } else { + val -= parseFloat( jQuery.css( elem, "border" + which[ i ] + "Width" ) ) || 0; + } + } + } + + return val + "px"; + } + + // Fall back to computed then uncomputed css if necessary + val = curCSS( elem, name, name ); + if ( val < 0 || val == null ) { + val = elem.style[ name ] || 0; + } + // Normalize "", auto, and prepare for extra + val = parseFloat( val ) || 0; + + // Add padding, border, margin + if ( extra ) { + for ( ; i < len; i++ ) { + val += parseFloat( jQuery.css( elem, "padding" + which[ i ] ) ) || 0; + if ( extra !== "padding" ) { + val += parseFloat( jQuery.css( elem, "border" + which[ i ] + "Width" ) ) || 0; + } + if ( extra === "margin" ) { + val += parseFloat( jQuery.css( elem, extra + which[ i ] ) ) || 0; + } + } + } + + return val + "px"; +} + +if ( jQuery.expr && jQuery.expr.filters ) { + jQuery.expr.filters.hidden = function( elem ) { + var width = elem.offsetWidth, + height = elem.offsetHeight; + + return ( width === 0 && height === 0 ) || (!jQuery.support.reliableHiddenOffsets && ((elem.style && elem.style.display) || jQuery.css( elem, "display" )) === "none"); + }; + + jQuery.expr.filters.visible = function( elem ) { + return !jQuery.expr.filters.hidden( elem ); + }; +} + + + + +var r20 = /%20/g, + rbracket = /\[\]$/, + rCRLF = /\r?\n/g, + rhash = /#.*$/, + rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, // IE leaves an \r character at EOL + rinput = /^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i, + // #7653, #8125, #8152: local protocol detection + rlocalProtocol = /^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/, + rnoContent = /^(?:GET|HEAD)$/, + rprotocol = /^\/\//, + rquery = /\?/, + rscript = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, + rselectTextarea = /^(?:select|textarea)/i, + rspacesAjax = /\s+/, + rts = /([?&])_=[^&]*/, + rurl = /^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/, + + // Keep a copy of the old load method + _load = jQuery.fn.load, + + /* Prefilters + * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example) + * 2) These are called: + * - BEFORE asking for a transport + * - AFTER param serialization (s.data is a string if s.processData is true) + * 3) key is the dataType + * 4) the catchall symbol "*" can be used + * 5) execution will start with transport dataType and THEN continue down to "*" if needed + */ + prefilters = {}, + + /* Transports bindings + * 1) key is the dataType + * 2) the catchall symbol "*" can be used + * 3) selection will start with transport dataType and THEN go to "*" if needed + */ + transports = {}, + + // Document location + ajaxLocation, + + // Document location segments + ajaxLocParts, + + // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression + allTypes = ["*/"] + ["*"]; + +// #8138, IE may throw an exception when accessing +// a field from window.location if document.domain has been set +try { + ajaxLocation = location.href; +} catch( e ) { + // Use the href attribute of an A element + // since IE will modify it given document.location + ajaxLocation = document.createElement( "a" ); + ajaxLocation.href = ""; + ajaxLocation = ajaxLocation.href; +} + +// Segment location into parts +ajaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || []; + +// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport +function addToPrefiltersOrTransports( structure ) { + + // dataTypeExpression is optional and defaults to "*" + return function( dataTypeExpression, func ) { + + if ( typeof dataTypeExpression !== "string" ) { + func = dataTypeExpression; + dataTypeExpression = "*"; + } + + if ( jQuery.isFunction( func ) ) { + var dataTypes = dataTypeExpression.toLowerCase().split( rspacesAjax ), + i = 0, + length = dataTypes.length, + dataType, + list, + placeBefore; + + // For each dataType in the dataTypeExpression + for ( ; i < length; i++ ) { + dataType = dataTypes[ i ]; + // We control if we're asked to add before + // any existing element + placeBefore = /^\+/.test( dataType ); + if ( placeBefore ) { + dataType = dataType.substr( 1 ) || "*"; + } + list = structure[ dataType ] = structure[ dataType ] || []; + // then we add to the structure accordingly + list[ placeBefore ? "unshift" : "push" ]( func ); + } + } + }; +} + +// Base inspection function for prefilters and transports +function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR, + dataType /* internal */, inspected /* internal */ ) { + + dataType = dataType || options.dataTypes[ 0 ]; + inspected = inspected || {}; + + inspected[ dataType ] = true; + + var list = structure[ dataType ], + i = 0, + length = list ? list.length : 0, + executeOnly = ( structure === prefilters ), + selection; + + for ( ; i < length && ( executeOnly || !selection ); i++ ) { + selection = list[ i ]( options, originalOptions, jqXHR ); + // If we got redirected to another dataType + // we try there if executing only and not done already + if ( typeof selection === "string" ) { + if ( !executeOnly || inspected[ selection ] ) { + selection = undefined; + } else { + options.dataTypes.unshift( selection ); + selection = inspectPrefiltersOrTransports( + structure, options, originalOptions, jqXHR, selection, inspected ); + } + } + } + // If we're only executing or nothing was selected + // we try the catchall dataType if not done already + if ( ( executeOnly || !selection ) && !inspected[ "*" ] ) { + selection = inspectPrefiltersOrTransports( + structure, options, originalOptions, jqXHR, "*", inspected ); + } + // unnecessary when only executing (prefilters) + // but it'll be ignored by the caller in that case + return selection; +} + +// A special extend for ajax options +// that takes "flat" options (not to be deep extended) +// Fixes #9887 +function ajaxExtend( target, src ) { + var key, deep, + flatOptions = jQuery.ajaxSettings.flatOptions || {}; + for ( key in src ) { + if ( src[ key ] !== undefined ) { + ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ]; + } + } + if ( deep ) { + jQuery.extend( true, target, deep ); + } +} + +jQuery.fn.extend({ + load: function( url, params, callback ) { + if ( typeof url !== "string" && _load ) { + return _load.apply( this, arguments ); + + // Don't do a request if no elements are being requested + } else if ( !this.length ) { + return this; + } + + var off = url.indexOf( " " ); + if ( off >= 0 ) { + var selector = url.slice( off, url.length ); + url = url.slice( 0, off ); + } + + // Default to a GET request + var type = "GET"; + + // If the second parameter was provided + if ( params ) { + // If it's a function + if ( jQuery.isFunction( params ) ) { + // We assume that it's the callback + callback = params; + params = undefined; + + // Otherwise, build a param string + } else if ( typeof params === "object" ) { + params = jQuery.param( params, jQuery.ajaxSettings.traditional ); + type = "POST"; + } + } + + var self = this; + + // Request the remote document + jQuery.ajax({ + url: url, + type: type, + dataType: "html", + data: params, + // Complete callback (responseText is used internally) + complete: function( jqXHR, status, responseText ) { + // Store the response as specified by the jqXHR object + responseText = jqXHR.responseText; + // If successful, inject the HTML into all the matched elements + if ( jqXHR.isResolved() ) { + // #4825: Get the actual response in case + // a dataFilter is present in ajaxSettings + jqXHR.done(function( r ) { + responseText = r; + }); + // See if a selector was specified + self.html( selector ? + // Create a dummy div to hold the results + jQuery("<div>") + // inject the contents of the document in, removing the scripts + // to avoid any 'Permission Denied' errors in IE + .append(responseText.replace(rscript, "")) + + // Locate the specified elements + .find(selector) : + + // If not, just inject the full result + responseText ); + } + + if ( callback ) { + self.each( callback, [ responseText, status, jqXHR ] ); + } + } + }); + + return this; + }, + + serialize: function() { + return jQuery.param( this.serializeArray() ); + }, + + serializeArray: function() { + return this.map(function(){ + return this.elements ? jQuery.makeArray( this.elements ) : this; + }) + .filter(function(){ + return this.name && !this.disabled && + ( this.checked || rselectTextarea.test( this.nodeName ) || + rinput.test( this.type ) ); + }) + .map(function( i, elem ){ + var val = jQuery( this ).val(); + + return val == null ? + null : + jQuery.isArray( val ) ? + jQuery.map( val, function( val, i ){ + return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + }) : + { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + }).get(); + } +}); + +// Attach a bunch of functions for handling common AJAX events +jQuery.each( "ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split( " " ), function( i, o ){ + jQuery.fn[ o ] = function( f ){ + return this.on( o, f ); + }; +}); + +jQuery.each( [ "get", "post" ], function( i, method ) { + jQuery[ method ] = function( url, data, callback, type ) { + // shift arguments if data argument was omitted + if ( jQuery.isFunction( data ) ) { + type = type || callback; + callback = data; + data = undefined; + } + + return jQuery.ajax({ + type: method, + url: url, + data: data, + success: callback, + dataType: type + }); + }; +}); + +jQuery.extend({ + + getScript: function( url, callback ) { + return jQuery.get( url, undefined, callback, "script" ); + }, + + getJSON: function( url, data, callback ) { + return jQuery.get( url, data, callback, "json" ); + }, + + // Creates a full fledged settings object into target + // with both ajaxSettings and settings fields. + // If target is omitted, writes into ajaxSettings. + ajaxSetup: function( target, settings ) { + if ( settings ) { + // Building a settings object + ajaxExtend( target, jQuery.ajaxSettings ); + } else { + // Extending ajaxSettings + settings = target; + target = jQuery.ajaxSettings; + } + ajaxExtend( target, settings ); + return target; + }, + + ajaxSettings: { + url: ajaxLocation, + isLocal: rlocalProtocol.test( ajaxLocParts[ 1 ] ), + global: true, + type: "GET", + contentType: "application/x-www-form-urlencoded", + processData: true, + async: true, + /* + timeout: 0, + data: null, + dataType: null, + username: null, + password: null, + cache: null, + traditional: false, + headers: {}, + */ + + accepts: { + xml: "application/xml, text/xml", + html: "text/html", + text: "text/plain", + json: "application/json, text/javascript", + "*": allTypes + }, + + contents: { + xml: /xml/, + html: /html/, + json: /json/ + }, + + responseFields: { + xml: "responseXML", + text: "responseText" + }, + + // List of data converters + // 1) key format is "source_type destination_type" (a single space in-between) + // 2) the catchall symbol "*" can be used for source_type + converters: { + + // Convert anything to text + "* text": window.String, + + // Text to html (true = no transformation) + "text html": true, + + // Evaluate text as a json expression + "text json": jQuery.parseJSON, + + // Parse text as xml + "text xml": jQuery.parseXML + }, + + // For options that shouldn't be deep extended: + // you can add your own custom options here if + // and when you create one that shouldn't be + // deep extended (see ajaxExtend) + flatOptions: { + context: true, + url: true + } + }, + + ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), + ajaxTransport: addToPrefiltersOrTransports( transports ), + + // Main method + ajax: function( url, options ) { + + // If url is an object, simulate pre-1.5 signature + if ( typeof url === "object" ) { + options = url; + url = undefined; + } + + // Force options to be an object + options = options || {}; + + var // Create the final options object + s = jQuery.ajaxSetup( {}, options ), + // Callbacks context + callbackContext = s.context || s, + // Context for global events + // It's the callbackContext if one was provided in the options + // and if it's a DOM node or a jQuery collection + globalEventContext = callbackContext !== s && + ( callbackContext.nodeType || callbackContext instanceof jQuery ) ? + jQuery( callbackContext ) : jQuery.event, + // Deferreds + deferred = jQuery.Deferred(), + completeDeferred = jQuery.Callbacks( "once memory" ), + // Status-dependent callbacks + statusCode = s.statusCode || {}, + // ifModified key + ifModifiedKey, + // Headers (they are sent all at once) + requestHeaders = {}, + requestHeadersNames = {}, + // Response headers + responseHeadersString, + responseHeaders, + // transport + transport, + // timeout handle + timeoutTimer, + // Cross-domain detection vars + parts, + // The jqXHR state + state = 0, + // To know if global events are to be dispatched + fireGlobals, + // Loop variable + i, + // Fake xhr + jqXHR = { + + readyState: 0, + + // Caches the header + setRequestHeader: function( name, value ) { + if ( !state ) { + var lname = name.toLowerCase(); + name = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name; + requestHeaders[ name ] = value; + } + return this; + }, + + // Raw string + getAllResponseHeaders: function() { + return state === 2 ? responseHeadersString : null; + }, + + // Builds headers hashtable if needed + getResponseHeader: function( key ) { + var match; + if ( state === 2 ) { + if ( !responseHeaders ) { + responseHeaders = {}; + while( ( match = rheaders.exec( responseHeadersString ) ) ) { + responseHeaders[ match[1].toLowerCase() ] = match[ 2 ]; + } + } + match = responseHeaders[ key.toLowerCase() ]; + } + return match === undefined ? null : match; + }, + + // Overrides response content-type header + overrideMimeType: function( type ) { + if ( !state ) { + s.mimeType = type; + } + return this; + }, + + // Cancel the request + abort: function( statusText ) { + statusText = statusText || "abort"; + if ( transport ) { + transport.abort( statusText ); + } + done( 0, statusText ); + return this; + } + }; + + // Callback for when everything is done + // It is defined here because jslint complains if it is declared + // at the end of the function (which would be more logical and readable) + function done( status, nativeStatusText, responses, headers ) { + + // Called once + if ( state === 2 ) { + return; + } + + // State is "done" now + state = 2; + + // Clear timeout if it exists + if ( timeoutTimer ) { + clearTimeout( timeoutTimer ); + } + + // Dereference transport for early garbage collection + // (no matter how long the jqXHR object will be used) + transport = undefined; + + // Cache response headers + responseHeadersString = headers || ""; + + // Set readyState + jqXHR.readyState = status > 0 ? 4 : 0; + + var isSuccess, + success, + error, + statusText = nativeStatusText, + response = responses ? ajaxHandleResponses( s, jqXHR, responses ) : undefined, + lastModified, + etag; + + // If successful, handle type chaining + if ( status >= 200 && status < 300 || status === 304 ) { + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + + if ( ( lastModified = jqXHR.getResponseHeader( "Last-Modified" ) ) ) { + jQuery.lastModified[ ifModifiedKey ] = lastModified; + } + if ( ( etag = jqXHR.getResponseHeader( "Etag" ) ) ) { + jQuery.etag[ ifModifiedKey ] = etag; + } + } + + // If not modified + if ( status === 304 ) { + + statusText = "notmodified"; + isSuccess = true; + + // If we have data + } else { + + try { + success = ajaxConvert( s, response ); + statusText = "success"; + isSuccess = true; + } catch(e) { + // We have a parsererror + statusText = "parsererror"; + error = e; + } + } + } else { + // We extract error from statusText + // then normalize statusText and status for non-aborts + error = statusText; + if ( !statusText || status ) { + statusText = "error"; + if ( status < 0 ) { + status = 0; + } + } + } + + // Set data for the fake xhr object + jqXHR.status = status; + jqXHR.statusText = "" + ( nativeStatusText || statusText ); + + // Success/Error + if ( isSuccess ) { + deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] ); + } else { + deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] ); + } + + // Status-dependent callbacks + jqXHR.statusCode( statusCode ); + statusCode = undefined; + + if ( fireGlobals ) { + globalEventContext.trigger( "ajax" + ( isSuccess ? "Success" : "Error" ), + [ jqXHR, s, isSuccess ? success : error ] ); + } + + // Complete + completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); + + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] ); + // Handle the global AJAX counter + if ( !( --jQuery.active ) ) { + jQuery.event.trigger( "ajaxStop" ); + } + } + } + + // Attach deferreds + deferred.promise( jqXHR ); + jqXHR.success = jqXHR.done; + jqXHR.error = jqXHR.fail; + jqXHR.complete = completeDeferred.add; + + // Status-dependent callbacks + jqXHR.statusCode = function( map ) { + if ( map ) { + var tmp; + if ( state < 2 ) { + for ( tmp in map ) { + statusCode[ tmp ] = [ statusCode[tmp], map[tmp] ]; + } + } else { + tmp = map[ jqXHR.status ]; + jqXHR.then( tmp, tmp ); + } + } + return this; + }; + + // Remove hash character (#7531: and string promotion) + // Add protocol if not provided (#5866: IE7 issue with protocol-less urls) + // We also use the url parameter if available + s.url = ( ( url || s.url ) + "" ).replace( rhash, "" ).replace( rprotocol, ajaxLocParts[ 1 ] + "//" ); + + // Extract dataTypes list + s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().split( rspacesAjax ); + + // Determine if a cross-domain request is in order + if ( s.crossDomain == null ) { + parts = rurl.exec( s.url.toLowerCase() ); + s.crossDomain = !!( parts && + ( parts[ 1 ] != ajaxLocParts[ 1 ] || parts[ 2 ] != ajaxLocParts[ 2 ] || + ( parts[ 3 ] || ( parts[ 1 ] === "http:" ? 80 : 443 ) ) != + ( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === "http:" ? 80 : 443 ) ) ) + ); + } + + // Convert data if not already a string + if ( s.data && s.processData && typeof s.data !== "string" ) { + s.data = jQuery.param( s.data, s.traditional ); + } + + // Apply prefilters + inspectPrefiltersOrTransports( prefilters, s, options, jqXHR ); + + // If request was aborted inside a prefiler, stop there + if ( state === 2 ) { + return false; + } + + // We can fire global events as of now if asked to + fireGlobals = s.global; + + // Uppercase the type + s.type = s.type.toUpperCase(); + + // Determine if request has content + s.hasContent = !rnoContent.test( s.type ); + + // Watch for a new set of requests + if ( fireGlobals && jQuery.active++ === 0 ) { + jQuery.event.trigger( "ajaxStart" ); + } + + // More options handling for requests with no content + if ( !s.hasContent ) { + + // If data is available, append data to url + if ( s.data ) { + s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.data; + // #9682: remove data so that it's not used in an eventual retry + delete s.data; + } + + // Get ifModifiedKey before adding the anti-cache parameter + ifModifiedKey = s.url; + + // Add anti-cache in url if needed + if ( s.cache === false ) { + + var ts = jQuery.now(), + // try replacing _= if it is there + ret = s.url.replace( rts, "$1_=" + ts ); + + // if nothing was replaced, add timestamp to the end + s.url = ret + ( ( ret === s.url ) ? ( rquery.test( s.url ) ? "&" : "?" ) + "_=" + ts : "" ); + } + } + + // Set the correct header, if data is being sent + if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { + jqXHR.setRequestHeader( "Content-Type", s.contentType ); + } + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + ifModifiedKey = ifModifiedKey || s.url; + if ( jQuery.lastModified[ ifModifiedKey ] ) { + jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ ifModifiedKey ] ); + } + if ( jQuery.etag[ ifModifiedKey ] ) { + jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ ifModifiedKey ] ); + } + } + + // Set the Accepts header for the server, depending on the dataType + jqXHR.setRequestHeader( + "Accept", + s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ? + s.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : + s.accepts[ "*" ] + ); + + // Check for headers option + for ( i in s.headers ) { + jqXHR.setRequestHeader( i, s.headers[ i ] ); + } + + // Allow custom headers/mimetypes and early abort + if ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) { + // Abort if not done already + jqXHR.abort(); + return false; + + } + + // Install callbacks on deferreds + for ( i in { success: 1, error: 1, complete: 1 } ) { + jqXHR[ i ]( s[ i ] ); + } + + // Get transport + transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR ); + + // If no transport, we auto-abort + if ( !transport ) { + done( -1, "No Transport" ); + } else { + jqXHR.readyState = 1; + // Send global event + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] ); + } + // Timeout + if ( s.async && s.timeout > 0 ) { + timeoutTimer = setTimeout( function(){ + jqXHR.abort( "timeout" ); + }, s.timeout ); + } + + try { + state = 1; + transport.send( requestHeaders, done ); + } catch (e) { + // Propagate exception as error if not done + if ( state < 2 ) { + done( -1, e ); + // Simply rethrow otherwise + } else { + throw e; + } + } + } + + return jqXHR; + }, + + // Serialize an array of form elements or a set of + // key/values into a query string + param: function( a, traditional ) { + var s = [], + add = function( key, value ) { + // If value is a function, invoke it and return its value + value = jQuery.isFunction( value ) ? value() : value; + s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value ); + }; + + // Set traditional to true for jQuery <= 1.3.2 behavior. + if ( traditional === undefined ) { + traditional = jQuery.ajaxSettings.traditional; + } + + // If an array was passed in, assume that it is an array of form elements. + if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { + // Serialize the form elements + jQuery.each( a, function() { + add( this.name, this.value ); + }); + + } else { + // If traditional, encode the "old" way (the way 1.3.2 or older + // did it), otherwise encode params recursively. + for ( var prefix in a ) { + buildParams( prefix, a[ prefix ], traditional, add ); + } + } + + // Return the resulting serialization + return s.join( "&" ).replace( r20, "+" ); + } +}); + +function buildParams( prefix, obj, traditional, add ) { + if ( jQuery.isArray( obj ) ) { + // Serialize array item. + jQuery.each( obj, function( i, v ) { + if ( traditional || rbracket.test( prefix ) ) { + // Treat each array item as a scalar. + add( prefix, v ); + + } else { + // If array item is non-scalar (array or object), encode its + // numeric index to resolve deserialization ambiguity issues. + // Note that rack (as of 1.0.0) can't currently deserialize + // nested arrays properly, and attempting to do so may cause + // a server error. Possible fixes are to modify rack's + // deserialization algorithm or to provide an option or flag + // to force array serialization to be shallow. + buildParams( prefix + "[" + ( typeof v === "object" || jQuery.isArray(v) ? i : "" ) + "]", v, traditional, add ); + } + }); + + } else if ( !traditional && obj != null && typeof obj === "object" ) { + // Serialize object item. + for ( var name in obj ) { + buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); + } + + } else { + // Serialize scalar item. + add( prefix, obj ); + } +} + +// This is still on the jQuery object... for now +// Want to move this to jQuery.ajax some day +jQuery.extend({ + + // Counter for holding the number of active queries + active: 0, + + // Last-Modified header cache for next request + lastModified: {}, + etag: {} + +}); + +/* Handles responses to an ajax request: + * - sets all responseXXX fields accordingly + * - finds the right dataType (mediates between content-type and expected dataType) + * - returns the corresponding response + */ +function ajaxHandleResponses( s, jqXHR, responses ) { + + var contents = s.contents, + dataTypes = s.dataTypes, + responseFields = s.responseFields, + ct, + type, + finalDataType, + firstDataType; + + // Fill responseXXX fields + for ( type in responseFields ) { + if ( type in responses ) { + jqXHR[ responseFields[type] ] = responses[ type ]; + } + } + + // Remove auto dataType and get content-type in the process + while( dataTypes[ 0 ] === "*" ) { + dataTypes.shift(); + if ( ct === undefined ) { + ct = s.mimeType || jqXHR.getResponseHeader( "content-type" ); + } + } + + // Check if we're dealing with a known content-type + if ( ct ) { + for ( type in contents ) { + if ( contents[ type ] && contents[ type ].test( ct ) ) { + dataTypes.unshift( type ); + break; + } + } + } + + // Check to see if we have a response for the expected dataType + if ( dataTypes[ 0 ] in responses ) { + finalDataType = dataTypes[ 0 ]; + } else { + // Try convertible dataTypes + for ( type in responses ) { + if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[0] ] ) { + finalDataType = type; + break; + } + if ( !firstDataType ) { + firstDataType = type; + } + } + // Or just use first one + finalDataType = finalDataType || firstDataType; + } + + // If we found a dataType + // We add the dataType to the list if needed + // and return the corresponding response + if ( finalDataType ) { + if ( finalDataType !== dataTypes[ 0 ] ) { + dataTypes.unshift( finalDataType ); + } + return responses[ finalDataType ]; + } +} + +// Chain conversions given the request and the original response +function ajaxConvert( s, response ) { + + // Apply the dataFilter if provided + if ( s.dataFilter ) { + response = s.dataFilter( response, s.dataType ); + } + + var dataTypes = s.dataTypes, + converters = {}, + i, + key, + length = dataTypes.length, + tmp, + // Current and previous dataTypes + current = dataTypes[ 0 ], + prev, + // Conversion expression + conversion, + // Conversion function + conv, + // Conversion functions (transitive conversion) + conv1, + conv2; + + // For each dataType in the chain + for ( i = 1; i < length; i++ ) { + + // Create converters map + // with lowercased keys + if ( i === 1 ) { + for ( key in s.converters ) { + if ( typeof key === "string" ) { + converters[ key.toLowerCase() ] = s.converters[ key ]; + } + } + } + + // Get the dataTypes + prev = current; + current = dataTypes[ i ]; + + // If current is auto dataType, update it to prev + if ( current === "*" ) { + current = prev; + // If no auto and dataTypes are actually different + } else if ( prev !== "*" && prev !== current ) { + + // Get the converter + conversion = prev + " " + current; + conv = converters[ conversion ] || converters[ "* " + current ]; + + // If there is no direct converter, search transitively + if ( !conv ) { + conv2 = undefined; + for ( conv1 in converters ) { + tmp = conv1.split( " " ); + if ( tmp[ 0 ] === prev || tmp[ 0 ] === "*" ) { + conv2 = converters[ tmp[1] + " " + current ]; + if ( conv2 ) { + conv1 = converters[ conv1 ]; + if ( conv1 === true ) { + conv = conv2; + } else if ( conv2 === true ) { + conv = conv1; + } + break; + } + } + } + } + // If we found no converter, dispatch an error + if ( !( conv || conv2 ) ) { + jQuery.error( "No conversion from " + conversion.replace(" "," to ") ); + } + // If found converter is not an equivalence + if ( conv !== true ) { + // Convert with 1 or 2 converters accordingly + response = conv ? conv( response ) : conv2( conv1(response) ); + } + } + } + return response; +} + + + + +var jsc = jQuery.now(), + jsre = /(\=)\?(&|$)|\?\?/i; + +// Default jsonp settings +jQuery.ajaxSetup({ + jsonp: "callback", + jsonpCallback: function() { + return jQuery.expando + "_" + ( jsc++ ); + } +}); + +// Detect, normalize options and install callbacks for jsonp requests +jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) { + + var inspectData = s.contentType === "application/x-www-form-urlencoded" && + ( typeof s.data === "string" ); + + if ( s.dataTypes[ 0 ] === "jsonp" || + s.jsonp !== false && ( jsre.test( s.url ) || + inspectData && jsre.test( s.data ) ) ) { + + var responseContainer, + jsonpCallback = s.jsonpCallback = + jQuery.isFunction( s.jsonpCallback ) ? s.jsonpCallback() : s.jsonpCallback, + previous = window[ jsonpCallback ], + url = s.url, + data = s.data, + replace = "$1" + jsonpCallback + "$2"; + + if ( s.jsonp !== false ) { + url = url.replace( jsre, replace ); + if ( s.url === url ) { + if ( inspectData ) { + data = data.replace( jsre, replace ); + } + if ( s.data === data ) { + // Add callback manually + url += (/\?/.test( url ) ? "&" : "?") + s.jsonp + "=" + jsonpCallback; + } + } + } + + s.url = url; + s.data = data; + + // Install callback + window[ jsonpCallback ] = function( response ) { + responseContainer = [ response ]; + }; + + // Clean-up function + jqXHR.always(function() { + // Set callback back to previous value + window[ jsonpCallback ] = previous; + // Call if it was a function and we have a response + if ( responseContainer && jQuery.isFunction( previous ) ) { + window[ jsonpCallback ]( responseContainer[ 0 ] ); + } + }); + + // Use data converter to retrieve json after script execution + s.converters["script json"] = function() { + if ( !responseContainer ) { + jQuery.error( jsonpCallback + " was not called" ); + } + return responseContainer[ 0 ]; + }; + + // force json dataType + s.dataTypes[ 0 ] = "json"; + + // Delegate to script + return "script"; + } +}); + + + + +// Install script dataType +jQuery.ajaxSetup({ + accepts: { + script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript" + }, + contents: { + script: /javascript|ecmascript/ + }, + converters: { + "text script": function( text ) { + jQuery.globalEval( text ); + return text; + } + } +}); + +// Handle cache's special case and global +jQuery.ajaxPrefilter( "script", function( s ) { + if ( s.cache === undefined ) { + s.cache = false; + } + if ( s.crossDomain ) { + s.type = "GET"; + s.global = false; + } +}); + +// Bind script tag hack transport +jQuery.ajaxTransport( "script", function(s) { + + // This transport only deals with cross domain requests + if ( s.crossDomain ) { + + var script, + head = document.head || document.getElementsByTagName( "head" )[0] || document.documentElement; + + return { + + send: function( _, callback ) { + + script = document.createElement( "script" ); + + script.async = "async"; + + if ( s.scriptCharset ) { + script.charset = s.scriptCharset; + } + + script.src = s.url; + + // Attach handlers for all browsers + script.onload = script.onreadystatechange = function( _, isAbort ) { + + if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) { + + // Handle memory leak in IE + script.onload = script.onreadystatechange = null; + + // Remove the script + if ( head && script.parentNode ) { + head.removeChild( script ); + } + + // Dereference the script + script = undefined; + + // Callback if not abort + if ( !isAbort ) { + callback( 200, "success" ); + } + } + }; + // Use insertBefore instead of appendChild to circumvent an IE6 bug. + // This arises when a base node is used (#2709 and #4378). + head.insertBefore( script, head.firstChild ); + }, + + abort: function() { + if ( script ) { + script.onload( 0, 1 ); + } + } + }; + } +}); + + + + +var // #5280: Internet Explorer will keep connections alive if we don't abort on unload + xhrOnUnloadAbort = window.ActiveXObject ? function() { + // Abort all pending requests + for ( var key in xhrCallbacks ) { + xhrCallbacks[ key ]( 0, 1 ); + } + } : false, + xhrId = 0, + xhrCallbacks; + +// Functions to create xhrs +function createStandardXHR() { + try { + return new window.XMLHttpRequest(); + } catch( e ) {} +} + +function createActiveXHR() { + try { + return new window.ActiveXObject( "Microsoft.XMLHTTP" ); + } catch( e ) {} +} + +// Create the request object +// (This is still attached to ajaxSettings for backward compatibility) +jQuery.ajaxSettings.xhr = window.ActiveXObject ? + /* Microsoft failed to properly + * implement the XMLHttpRequest in IE7 (can't request local files), + * so we use the ActiveXObject when it is available + * Additionally XMLHttpRequest can be disabled in IE7/IE8 so + * we need a fallback. + */ + function() { + return !this.isLocal && createStandardXHR() || createActiveXHR(); + } : + // For all other browsers, use the standard XMLHttpRequest object + createStandardXHR; + +// Determine support properties +(function( xhr ) { + jQuery.extend( jQuery.support, { + ajax: !!xhr, + cors: !!xhr && ( "withCredentials" in xhr ) + }); +})( jQuery.ajaxSettings.xhr() ); + +// Create transport if the browser can provide an xhr +if ( jQuery.support.ajax ) { + + jQuery.ajaxTransport(function( s ) { + // Cross domain only allowed if supported through XMLHttpRequest + if ( !s.crossDomain || jQuery.support.cors ) { + + var callback; + + return { + send: function( headers, complete ) { + + // Get a new xhr + var xhr = s.xhr(), + handle, + i; + + // Open the socket + // Passing null username, generates a login popup on Opera (#2865) + if ( s.username ) { + xhr.open( s.type, s.url, s.async, s.username, s.password ); + } else { + xhr.open( s.type, s.url, s.async ); + } + + // Apply custom fields if provided + if ( s.xhrFields ) { + for ( i in s.xhrFields ) { + xhr[ i ] = s.xhrFields[ i ]; + } + } + + // Override mime type if needed + if ( s.mimeType && xhr.overrideMimeType ) { + xhr.overrideMimeType( s.mimeType ); + } + + // X-Requested-With header + // For cross-domain requests, seeing as conditions for a preflight are + // akin to a jigsaw puzzle, we simply never set it to be sure. + // (it can always be set on a per-request basis or even using ajaxSetup) + // For same-domain requests, won't change header if already provided. + if ( !s.crossDomain && !headers["X-Requested-With"] ) { + headers[ "X-Requested-With" ] = "XMLHttpRequest"; + } + + // Need an extra try/catch for cross domain requests in Firefox 3 + try { + for ( i in headers ) { + xhr.setRequestHeader( i, headers[ i ] ); + } + } catch( _ ) {} + + // Do send the request + // This may raise an exception which is actually + // handled in jQuery.ajax (so no try/catch here) + xhr.send( ( s.hasContent && s.data ) || null ); + + // Listener + callback = function( _, isAbort ) { + + var status, + statusText, + responseHeaders, + responses, + xml; + + // Firefox throws exceptions when accessing properties + // of an xhr when a network error occured + // http://helpful.knobs-dials.com/index.php/Component_returned_failure_code:_0x80040111_(NS_ERROR_NOT_AVAILABLE) + try { + + // Was never called and is aborted or complete + if ( callback && ( isAbort || xhr.readyState === 4 ) ) { + + // Only called once + callback = undefined; + + // Do not keep as active anymore + if ( handle ) { + xhr.onreadystatechange = jQuery.noop; + if ( xhrOnUnloadAbort ) { + delete xhrCallbacks[ handle ]; + } + } + + // If it's an abort + if ( isAbort ) { + // Abort it manually if needed + if ( xhr.readyState !== 4 ) { + xhr.abort(); + } + } else { + status = xhr.status; + responseHeaders = xhr.getAllResponseHeaders(); + responses = {}; + xml = xhr.responseXML; + + // Construct response list + if ( xml && xml.documentElement /* #4958 */ ) { + responses.xml = xml; + } + responses.text = xhr.responseText; + + // Firefox throws an exception when accessing + // statusText for faulty cross-domain requests + try { + statusText = xhr.statusText; + } catch( e ) { + // We normalize with Webkit giving an empty statusText + statusText = ""; + } + + // Filter status for non standard behaviors + + // If the request is local and we have data: assume a success + // (success with no data won't get notified, that's the best we + // can do given current implementations) + if ( !status && s.isLocal && !s.crossDomain ) { + status = responses.text ? 200 : 404; + // IE - #1450: sometimes returns 1223 when it should be 204 + } else if ( status === 1223 ) { + status = 204; + } + } + } + } catch( firefoxAccessException ) { + if ( !isAbort ) { + complete( -1, firefoxAccessException ); + } + } + + // Call complete if needed + if ( responses ) { + complete( status, statusText, responses, responseHeaders ); + } + }; + + // if we're in sync mode or it's in cache + // and has been retrieved directly (IE6 & IE7) + // we need to manually fire the callback + if ( !s.async || xhr.readyState === 4 ) { + callback(); + } else { + handle = ++xhrId; + if ( xhrOnUnloadAbort ) { + // Create the active xhrs callbacks list if needed + // and attach the unload handler + if ( !xhrCallbacks ) { + xhrCallbacks = {}; + jQuery( window ).unload( xhrOnUnloadAbort ); + } + // Add to list of active xhrs callbacks + xhrCallbacks[ handle ] = callback; + } + xhr.onreadystatechange = callback; + } + }, + + abort: function() { + if ( callback ) { + callback(0,1); + } + } + }; + } + }); +} + + + + +var elemdisplay = {}, + iframe, iframeDoc, + rfxtypes = /^(?:toggle|show|hide)$/, + rfxnum = /^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i, + timerId, + fxAttrs = [ + // height animations + [ "height", "marginTop", "marginBottom", "paddingTop", "paddingBottom" ], + // width animations + [ "width", "marginLeft", "marginRight", "paddingLeft", "paddingRight" ], + // opacity animations + [ "opacity" ] + ], + fxNow; + +jQuery.fn.extend({ + show: function( speed, easing, callback ) { + var elem, display; + + if ( speed || speed === 0 ) { + return this.animate( genFx("show", 3), speed, easing, callback ); + + } else { + for ( var i = 0, j = this.length; i < j; i++ ) { + elem = this[ i ]; + + if ( elem.style ) { + display = elem.style.display; + + // Reset the inline display of this element to learn if it is + // being hidden by cascaded rules or not + if ( !jQuery._data(elem, "olddisplay") && display === "none" ) { + display = elem.style.display = ""; + } + + // Set elements which have been overridden with display: none + // in a stylesheet to whatever the default browser style is + // for such an element + if ( display === "" && jQuery.css(elem, "display") === "none" ) { + jQuery._data( elem, "olddisplay", defaultDisplay(elem.nodeName) ); + } + } + } + + // Set the display of most of the elements in a second loop + // to avoid the constant reflow + for ( i = 0; i < j; i++ ) { + elem = this[ i ]; + + if ( elem.style ) { + display = elem.style.display; + + if ( display === "" || display === "none" ) { + elem.style.display = jQuery._data( elem, "olddisplay" ) || ""; + } + } + } + + return this; + } + }, + + hide: function( speed, easing, callback ) { + if ( speed || speed === 0 ) { + return this.animate( genFx("hide", 3), speed, easing, callback); + + } else { + var elem, display, + i = 0, + j = this.length; + + for ( ; i < j; i++ ) { + elem = this[i]; + if ( elem.style ) { + display = jQuery.css( elem, "display" ); + + if ( display !== "none" && !jQuery._data( elem, "olddisplay" ) ) { + jQuery._data( elem, "olddisplay", display ); + } + } + } + + // Set the display of the elements in a second loop + // to avoid the constant reflow + for ( i = 0; i < j; i++ ) { + if ( this[i].style ) { + this[i].style.display = "none"; + } + } + + return this; + } + }, + + // Save the old toggle function + _toggle: jQuery.fn.toggle, + + toggle: function( fn, fn2, callback ) { + var bool = typeof fn === "boolean"; + + if ( jQuery.isFunction(fn) && jQuery.isFunction(fn2) ) { + this._toggle.apply( this, arguments ); + + } else if ( fn == null || bool ) { + this.each(function() { + var state = bool ? fn : jQuery(this).is(":hidden"); + jQuery(this)[ state ? "show" : "hide" ](); + }); + + } else { + this.animate(genFx("toggle", 3), fn, fn2, callback); + } + + return this; + }, + + fadeTo: function( speed, to, easing, callback ) { + return this.filter(":hidden").css("opacity", 0).show().end() + .animate({opacity: to}, speed, easing, callback); + }, + + animate: function( prop, speed, easing, callback ) { + var optall = jQuery.speed( speed, easing, callback ); + + if ( jQuery.isEmptyObject( prop ) ) { + return this.each( optall.complete, [ false ] ); + } + + // Do not change referenced properties as per-property easing will be lost + prop = jQuery.extend( {}, prop ); + + function doAnimation() { + // XXX 'this' does not always have a nodeName when running the + // test suite + + if ( optall.queue === false ) { + jQuery._mark( this ); + } + + var opt = jQuery.extend( {}, optall ), + isElement = this.nodeType === 1, + hidden = isElement && jQuery(this).is(":hidden"), + name, val, p, e, + parts, start, end, unit, + method; + + // will store per property easing and be used to determine when an animation is complete + opt.animatedProperties = {}; + + for ( p in prop ) { + + // property name normalization + name = jQuery.camelCase( p ); + if ( p !== name ) { + prop[ name ] = prop[ p ]; + delete prop[ p ]; + } + + val = prop[ name ]; + + // easing resolution: per property > opt.specialEasing > opt.easing > 'swing' (default) + if ( jQuery.isArray( val ) ) { + opt.animatedProperties[ name ] = val[ 1 ]; + val = prop[ name ] = val[ 0 ]; + } else { + opt.animatedProperties[ name ] = opt.specialEasing && opt.specialEasing[ name ] || opt.easing || 'swing'; + } + + if ( val === "hide" && hidden || val === "show" && !hidden ) { + return opt.complete.call( this ); + } + + if ( isElement && ( name === "height" || name === "width" ) ) { + // Make sure that nothing sneaks out + // Record all 3 overflow attributes because IE does not + // change the overflow attribute when overflowX and + // overflowY are set to the same value + opt.overflow = [ this.style.overflow, this.style.overflowX, this.style.overflowY ]; + + // Set display property to inline-block for height/width + // animations on inline elements that are having width/height animated + if ( jQuery.css( this, "display" ) === "inline" && + jQuery.css( this, "float" ) === "none" ) { + + // inline-level elements accept inline-block; + // block-level elements need to be inline with layout + if ( !jQuery.support.inlineBlockNeedsLayout || defaultDisplay( this.nodeName ) === "inline" ) { + this.style.display = "inline-block"; + + } else { + this.style.zoom = 1; + } + } + } + } + + if ( opt.overflow != null ) { + this.style.overflow = "hidden"; + } + + for ( p in prop ) { + e = new jQuery.fx( this, opt, p ); + val = prop[ p ]; + + if ( rfxtypes.test( val ) ) { + + // Tracks whether to show or hide based on private + // data attached to the element + method = jQuery._data( this, "toggle" + p ) || ( val === "toggle" ? hidden ? "show" : "hide" : 0 ); + if ( method ) { + jQuery._data( this, "toggle" + p, method === "show" ? "hide" : "show" ); + e[ method ](); + } else { + e[ val ](); + } + + } else { + parts = rfxnum.exec( val ); + start = e.cur(); + + if ( parts ) { + end = parseFloat( parts[2] ); + unit = parts[3] || ( jQuery.cssNumber[ p ] ? "" : "px" ); + + // We need to compute starting value + if ( unit !== "px" ) { + jQuery.style( this, p, (end || 1) + unit); + start = ( (end || 1) / e.cur() ) * start; + jQuery.style( this, p, start + unit); + } + + // If a +=/-= token was provided, we're doing a relative animation + if ( parts[1] ) { + end = ( (parts[ 1 ] === "-=" ? -1 : 1) * end ) + start; + } + + e.custom( start, end, unit ); + + } else { + e.custom( start, val, "" ); + } + } + } + + // For JS strict compliance + return true; + } + + return optall.queue === false ? + this.each( doAnimation ) : + this.queue( optall.queue, doAnimation ); + }, + + stop: function( type, clearQueue, gotoEnd ) { + if ( typeof type !== "string" ) { + gotoEnd = clearQueue; + clearQueue = type; + type = undefined; + } + if ( clearQueue && type !== false ) { + this.queue( type || "fx", [] ); + } + + return this.each(function() { + var index, + hadTimers = false, + timers = jQuery.timers, + data = jQuery._data( this ); + + // clear marker counters if we know they won't be + if ( !gotoEnd ) { + jQuery._unmark( true, this ); + } + + function stopQueue( elem, data, index ) { + var hooks = data[ index ]; + jQuery.removeData( elem, index, true ); + hooks.stop( gotoEnd ); + } + + if ( type == null ) { + for ( index in data ) { + if ( data[ index ] && data[ index ].stop && index.indexOf(".run") === index.length - 4 ) { + stopQueue( this, data, index ); + } + } + } else if ( data[ index = type + ".run" ] && data[ index ].stop ){ + stopQueue( this, data, index ); + } + + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) { + if ( gotoEnd ) { + + // force the next step to be the last + timers[ index ]( true ); + } else { + timers[ index ].saveState(); + } + hadTimers = true; + timers.splice( index, 1 ); + } + } + + // start the next in the queue if the last step wasn't forced + // timers currently will call their complete callbacks, which will dequeue + // but only if they were gotoEnd + if ( !( gotoEnd && hadTimers ) ) { + jQuery.dequeue( this, type ); + } + }); + } + +}); + +// Animations created synchronously will run synchronously +function createFxNow() { + setTimeout( clearFxNow, 0 ); + return ( fxNow = jQuery.now() ); +} + +function clearFxNow() { + fxNow = undefined; +} + +// Generate parameters to create a standard animation +function genFx( type, num ) { + var obj = {}; + + jQuery.each( fxAttrs.concat.apply([], fxAttrs.slice( 0, num )), function() { + obj[ this ] = type; + }); + + return obj; +} + +// Generate shortcuts for custom animations +jQuery.each({ + slideDown: genFx( "show", 1 ), + slideUp: genFx( "hide", 1 ), + slideToggle: genFx( "toggle", 1 ), + fadeIn: { opacity: "show" }, + fadeOut: { opacity: "hide" }, + fadeToggle: { opacity: "toggle" } +}, function( name, props ) { + jQuery.fn[ name ] = function( speed, easing, callback ) { + return this.animate( props, speed, easing, callback ); + }; +}); + +jQuery.extend({ + speed: function( speed, easing, fn ) { + var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { + complete: fn || !fn && easing || + jQuery.isFunction( speed ) && speed, + duration: speed, + easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing + }; + + opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration : + opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default; + + // normalize opt.queue - true/undefined/null -> "fx" + if ( opt.queue == null || opt.queue === true ) { + opt.queue = "fx"; + } + + // Queueing + opt.old = opt.complete; + + opt.complete = function( noUnmark ) { + if ( jQuery.isFunction( opt.old ) ) { + opt.old.call( this ); + } + + if ( opt.queue ) { + jQuery.dequeue( this, opt.queue ); + } else if ( noUnmark !== false ) { + jQuery._unmark( this ); + } + }; + + return opt; + }, + + easing: { + linear: function( p, n, firstNum, diff ) { + return firstNum + diff * p; + }, + swing: function( p, n, firstNum, diff ) { + return ( ( -Math.cos( p*Math.PI ) / 2 ) + 0.5 ) * diff + firstNum; + } + }, + + timers: [], + + fx: function( elem, options, prop ) { + this.options = options; + this.elem = elem; + this.prop = prop; + + options.orig = options.orig || {}; + } + +}); + +jQuery.fx.prototype = { + // Simple function for setting a style value + update: function() { + if ( this.options.step ) { + this.options.step.call( this.elem, this.now, this ); + } + + ( jQuery.fx.step[ this.prop ] || jQuery.fx.step._default )( this ); + }, + + // Get the current size + cur: function() { + if ( this.elem[ this.prop ] != null && (!this.elem.style || this.elem.style[ this.prop ] == null) ) { + return this.elem[ this.prop ]; + } + + var parsed, + r = jQuery.css( this.elem, this.prop ); + // Empty strings, null, undefined and "auto" are converted to 0, + // complex values such as "rotate(1rad)" are returned as is, + // simple values such as "10px" are parsed to Float. + return isNaN( parsed = parseFloat( r ) ) ? !r || r === "auto" ? 0 : r : parsed; + }, + + // Start an animation from one number to another + custom: function( from, to, unit ) { + var self = this, + fx = jQuery.fx; + + this.startTime = fxNow || createFxNow(); + this.end = to; + this.now = this.start = from; + this.pos = this.state = 0; + this.unit = unit || this.unit || ( jQuery.cssNumber[ this.prop ] ? "" : "px" ); + + function t( gotoEnd ) { + return self.step( gotoEnd ); + } + + t.queue = this.options.queue; + t.elem = this.elem; + t.saveState = function() { + if ( self.options.hide && jQuery._data( self.elem, "fxshow" + self.prop ) === undefined ) { + jQuery._data( self.elem, "fxshow" + self.prop, self.start ); + } + }; + + if ( t() && jQuery.timers.push(t) && !timerId ) { + timerId = setInterval( fx.tick, fx.interval ); + } + }, + + // Simple 'show' function + show: function() { + var dataShow = jQuery._data( this.elem, "fxshow" + this.prop ); + + // Remember where we started, so that we can go back to it later + this.options.orig[ this.prop ] = dataShow || jQuery.style( this.elem, this.prop ); + this.options.show = true; + + // Begin the animation + // Make sure that we start at a small width/height to avoid any flash of content + if ( dataShow !== undefined ) { + // This show is picking up where a previous hide or show left off + this.custom( this.cur(), dataShow ); + } else { + this.custom( this.prop === "width" || this.prop === "height" ? 1 : 0, this.cur() ); + } + + // Start by showing the element + jQuery( this.elem ).show(); + }, + + // Simple 'hide' function + hide: function() { + // Remember where we started, so that we can go back to it later + this.options.orig[ this.prop ] = jQuery._data( this.elem, "fxshow" + this.prop ) || jQuery.style( this.elem, this.prop ); + this.options.hide = true; + + // Begin the animation + this.custom( this.cur(), 0 ); + }, + + // Each step of an animation + step: function( gotoEnd ) { + var p, n, complete, + t = fxNow || createFxNow(), + done = true, + elem = this.elem, + options = this.options; + + if ( gotoEnd || t >= options.duration + this.startTime ) { + this.now = this.end; + this.pos = this.state = 1; + this.update(); + + options.animatedProperties[ this.prop ] = true; + + for ( p in options.animatedProperties ) { + if ( options.animatedProperties[ p ] !== true ) { + done = false; + } + } + + if ( done ) { + // Reset the overflow + if ( options.overflow != null && !jQuery.support.shrinkWrapBlocks ) { + + jQuery.each( [ "", "X", "Y" ], function( index, value ) { + elem.style[ "overflow" + value ] = options.overflow[ index ]; + }); + } + + // Hide the element if the "hide" operation was done + if ( options.hide ) { + jQuery( elem ).hide(); + } + + // Reset the properties, if the item has been hidden or shown + if ( options.hide || options.show ) { + for ( p in options.animatedProperties ) { + jQuery.style( elem, p, options.orig[ p ] ); + jQuery.removeData( elem, "fxshow" + p, true ); + // Toggle data is no longer needed + jQuery.removeData( elem, "toggle" + p, true ); + } + } + + // Execute the complete function + // in the event that the complete function throws an exception + // we must ensure it won't be called twice. #5684 + + complete = options.complete; + if ( complete ) { + + options.complete = false; + complete.call( elem ); + } + } + + return false; + + } else { + // classical easing cannot be used with an Infinity duration + if ( options.duration == Infinity ) { + this.now = t; + } else { + n = t - this.startTime; + this.state = n / options.duration; + + // Perform the easing function, defaults to swing + this.pos = jQuery.easing[ options.animatedProperties[this.prop] ]( this.state, n, 0, 1, options.duration ); + this.now = this.start + ( (this.end - this.start) * this.pos ); + } + // Perform the next step of the animation + this.update(); + } + + return true; + } +}; + +jQuery.extend( jQuery.fx, { + tick: function() { + var timer, + timers = jQuery.timers, + i = 0; + + for ( ; i < timers.length; i++ ) { + timer = timers[ i ]; + // Checks the timer has not already been removed + if ( !timer() && timers[ i ] === timer ) { + timers.splice( i--, 1 ); + } + } + + if ( !timers.length ) { + jQuery.fx.stop(); + } + }, + + interval: 13, + + stop: function() { + clearInterval( timerId ); + timerId = null; + }, + + speeds: { + slow: 600, + fast: 200, + // Default speed + _default: 400 + }, + + step: { + opacity: function( fx ) { + jQuery.style( fx.elem, "opacity", fx.now ); + }, + + _default: function( fx ) { + if ( fx.elem.style && fx.elem.style[ fx.prop ] != null ) { + fx.elem.style[ fx.prop ] = fx.now + fx.unit; + } else { + fx.elem[ fx.prop ] = fx.now; + } + } + } +}); + +// Adds width/height step functions +// Do not set anything below 0 +jQuery.each([ "width", "height" ], function( i, prop ) { + jQuery.fx.step[ prop ] = function( fx ) { + jQuery.style( fx.elem, prop, Math.max(0, fx.now) + fx.unit ); + }; +}); + +if ( jQuery.expr && jQuery.expr.filters ) { + jQuery.expr.filters.animated = function( elem ) { + return jQuery.grep(jQuery.timers, function( fn ) { + return elem === fn.elem; + }).length; + }; +} + +// Try to restore the default display value of an element +function defaultDisplay( nodeName ) { + + if ( !elemdisplay[ nodeName ] ) { + + var body = document.body, + elem = jQuery( "<" + nodeName + ">" ).appendTo( body ), + display = elem.css( "display" ); + elem.remove(); + + // If the simple way fails, + // get element's real default display by attaching it to a temp iframe + if ( display === "none" || display === "" ) { + // No iframe to use yet, so create it + if ( !iframe ) { + iframe = document.createElement( "iframe" ); + iframe.frameBorder = iframe.width = iframe.height = 0; + } + + body.appendChild( iframe ); + + // Create a cacheable copy of the iframe document on first call. + // IE and Opera will allow us to reuse the iframeDoc without re-writing the fake HTML + // document to it; WebKit & Firefox won't allow reusing the iframe document. + if ( !iframeDoc || !iframe.createElement ) { + iframeDoc = ( iframe.contentWindow || iframe.contentDocument ).document; + iframeDoc.write( ( document.compatMode === "CSS1Compat" ? "<!doctype html>" : "" ) + "<html><body>" ); + iframeDoc.close(); + } + + elem = iframeDoc.createElement( nodeName ); + + iframeDoc.body.appendChild( elem ); + + display = jQuery.css( elem, "display" ); + body.removeChild( iframe ); + } + + // Store the correct default display + elemdisplay[ nodeName ] = display; + } + + return elemdisplay[ nodeName ]; +} + + + + +var rtable = /^t(?:able|d|h)$/i, + rroot = /^(?:body|html)$/i; + +if ( "getBoundingClientRect" in document.documentElement ) { + jQuery.fn.offset = function( options ) { + var elem = this[0], box; + + if ( options ) { + return this.each(function( i ) { + jQuery.offset.setOffset( this, options, i ); + }); + } + + if ( !elem || !elem.ownerDocument ) { + return null; + } + + if ( elem === elem.ownerDocument.body ) { + return jQuery.offset.bodyOffset( elem ); + } + + try { + box = elem.getBoundingClientRect(); + } catch(e) {} + + var doc = elem.ownerDocument, + docElem = doc.documentElement; + + // Make sure we're not dealing with a disconnected DOM node + if ( !box || !jQuery.contains( docElem, elem ) ) { + return box ? { top: box.top, left: box.left } : { top: 0, left: 0 }; + } + + var body = doc.body, + win = getWindow(doc), + clientTop = docElem.clientTop || body.clientTop || 0, + clientLeft = docElem.clientLeft || body.clientLeft || 0, + scrollTop = win.pageYOffset || jQuery.support.boxModel && docElem.scrollTop || body.scrollTop, + scrollLeft = win.pageXOffset || jQuery.support.boxModel && docElem.scrollLeft || body.scrollLeft, + top = box.top + scrollTop - clientTop, + left = box.left + scrollLeft - clientLeft; + + return { top: top, left: left }; + }; + +} else { + jQuery.fn.offset = function( options ) { + var elem = this[0]; + + if ( options ) { + return this.each(function( i ) { + jQuery.offset.setOffset( this, options, i ); + }); + } + + if ( !elem || !elem.ownerDocument ) { + return null; + } + + if ( elem === elem.ownerDocument.body ) { + return jQuery.offset.bodyOffset( elem ); + } + + var computedStyle, + offsetParent = elem.offsetParent, + prevOffsetParent = elem, + doc = elem.ownerDocument, + docElem = doc.documentElement, + body = doc.body, + defaultView = doc.defaultView, + prevComputedStyle = defaultView ? defaultView.getComputedStyle( elem, null ) : elem.currentStyle, + top = elem.offsetTop, + left = elem.offsetLeft; + + while ( (elem = elem.parentNode) && elem !== body && elem !== docElem ) { + if ( jQuery.support.fixedPosition && prevComputedStyle.position === "fixed" ) { + break; + } + + computedStyle = defaultView ? defaultView.getComputedStyle(elem, null) : elem.currentStyle; + top -= elem.scrollTop; + left -= elem.scrollLeft; + + if ( elem === offsetParent ) { + top += elem.offsetTop; + left += elem.offsetLeft; + + if ( jQuery.support.doesNotAddBorder && !(jQuery.support.doesAddBorderForTableAndCells && rtable.test(elem.nodeName)) ) { + top += parseFloat( computedStyle.borderTopWidth ) || 0; + left += parseFloat( computedStyle.borderLeftWidth ) || 0; + } + + prevOffsetParent = offsetParent; + offsetParent = elem.offsetParent; + } + + if ( jQuery.support.subtractsBorderForOverflowNotVisible && computedStyle.overflow !== "visible" ) { + top += parseFloat( computedStyle.borderTopWidth ) || 0; + left += parseFloat( computedStyle.borderLeftWidth ) || 0; + } + + prevComputedStyle = computedStyle; + } + + if ( prevComputedStyle.position === "relative" || prevComputedStyle.position === "static" ) { + top += body.offsetTop; + left += body.offsetLeft; + } + + if ( jQuery.support.fixedPosition && prevComputedStyle.position === "fixed" ) { + top += Math.max( docElem.scrollTop, body.scrollTop ); + left += Math.max( docElem.scrollLeft, body.scrollLeft ); + } + + return { top: top, left: left }; + }; +} + +jQuery.offset = { + + bodyOffset: function( body ) { + var top = body.offsetTop, + left = body.offsetLeft; + + if ( jQuery.support.doesNotIncludeMarginInBodyOffset ) { + top += parseFloat( jQuery.css(body, "marginTop") ) || 0; + left += parseFloat( jQuery.css(body, "marginLeft") ) || 0; + } + + return { top: top, left: left }; + }, + + setOffset: function( elem, options, i ) { + var position = jQuery.css( elem, "position" ); + + // set position first, in-case top/left are set even on static elem + if ( position === "static" ) { + elem.style.position = "relative"; + } + + var curElem = jQuery( elem ), + curOffset = curElem.offset(), + curCSSTop = jQuery.css( elem, "top" ), + curCSSLeft = jQuery.css( elem, "left" ), + calculatePosition = ( position === "absolute" || position === "fixed" ) && jQuery.inArray("auto", [curCSSTop, curCSSLeft]) > -1, + props = {}, curPosition = {}, curTop, curLeft; + + // need to be able to calculate position if either top or left is auto and position is either absolute or fixed + if ( calculatePosition ) { + curPosition = curElem.position(); + curTop = curPosition.top; + curLeft = curPosition.left; + } else { + curTop = parseFloat( curCSSTop ) || 0; + curLeft = parseFloat( curCSSLeft ) || 0; + } + + if ( jQuery.isFunction( options ) ) { + options = options.call( elem, i, curOffset ); + } + + if ( options.top != null ) { + props.top = ( options.top - curOffset.top ) + curTop; + } + if ( options.left != null ) { + props.left = ( options.left - curOffset.left ) + curLeft; + } + + if ( "using" in options ) { + options.using.call( elem, props ); + } else { + curElem.css( props ); + } + } +}; + + +jQuery.fn.extend({ + + position: function() { + if ( !this[0] ) { + return null; + } + + var elem = this[0], + + // Get *real* offsetParent + offsetParent = this.offsetParent(), + + // Get correct offsets + offset = this.offset(), + parentOffset = rroot.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset(); + + // Subtract element margins + // note: when an element has margin: auto the offsetLeft and marginLeft + // are the same in Safari causing offset.left to incorrectly be 0 + offset.top -= parseFloat( jQuery.css(elem, "marginTop") ) || 0; + offset.left -= parseFloat( jQuery.css(elem, "marginLeft") ) || 0; + + // Add offsetParent borders + parentOffset.top += parseFloat( jQuery.css(offsetParent[0], "borderTopWidth") ) || 0; + parentOffset.left += parseFloat( jQuery.css(offsetParent[0], "borderLeftWidth") ) || 0; + + // Subtract the two offsets + return { + top: offset.top - parentOffset.top, + left: offset.left - parentOffset.left + }; + }, + + offsetParent: function() { + return this.map(function() { + var offsetParent = this.offsetParent || document.body; + while ( offsetParent && (!rroot.test(offsetParent.nodeName) && jQuery.css(offsetParent, "position") === "static") ) { + offsetParent = offsetParent.offsetParent; + } + return offsetParent; + }); + } +}); + + +// Create scrollLeft and scrollTop methods +jQuery.each( ["Left", "Top"], function( i, name ) { + var method = "scroll" + name; + + jQuery.fn[ method ] = function( val ) { + var elem, win; + + if ( val === undefined ) { + elem = this[ 0 ]; + + if ( !elem ) { + return null; + } + + win = getWindow( elem ); + + // Return the scroll offset + return win ? ("pageXOffset" in win) ? win[ i ? "pageYOffset" : "pageXOffset" ] : + jQuery.support.boxModel && win.document.documentElement[ method ] || + win.document.body[ method ] : + elem[ method ]; + } + + // Set the scroll offset + return this.each(function() { + win = getWindow( this ); + + if ( win ) { + win.scrollTo( + !i ? val : jQuery( win ).scrollLeft(), + i ? val : jQuery( win ).scrollTop() + ); + + } else { + this[ method ] = val; + } + }); + }; +}); + +function getWindow( elem ) { + return jQuery.isWindow( elem ) ? + elem : + elem.nodeType === 9 ? + elem.defaultView || elem.parentWindow : + false; +} + + + + +// Create width, height, innerHeight, innerWidth, outerHeight and outerWidth methods +jQuery.each([ "Height", "Width" ], function( i, name ) { + + var type = name.toLowerCase(); + + // innerHeight and innerWidth + jQuery.fn[ "inner" + name ] = function() { + var elem = this[0]; + return elem ? + elem.style ? + parseFloat( jQuery.css( elem, type, "padding" ) ) : + this[ type ]() : + null; + }; + + // outerHeight and outerWidth + jQuery.fn[ "outer" + name ] = function( margin ) { + var elem = this[0]; + return elem ? + elem.style ? + parseFloat( jQuery.css( elem, type, margin ? "margin" : "border" ) ) : + this[ type ]() : + null; + }; + + jQuery.fn[ type ] = function( size ) { + // Get window width or height + var elem = this[0]; + if ( !elem ) { + return size == null ? null : this; + } + + if ( jQuery.isFunction( size ) ) { + return this.each(function( i ) { + var self = jQuery( this ); + self[ type ]( size.call( this, i, self[ type ]() ) ); + }); + } + + if ( jQuery.isWindow( elem ) ) { + // Everyone else use document.documentElement or document.body depending on Quirks vs Standards mode + // 3rd condition allows Nokia support, as it supports the docElem prop but not CSS1Compat + var docElemProp = elem.document.documentElement[ "client" + name ], + body = elem.document.body; + return elem.document.compatMode === "CSS1Compat" && docElemProp || + body && body[ "client" + name ] || docElemProp; + + // Get document width or height + } else if ( elem.nodeType === 9 ) { + // Either scroll[Width/Height] or offset[Width/Height], whichever is greater + return Math.max( + elem.documentElement["client" + name], + elem.body["scroll" + name], elem.documentElement["scroll" + name], + elem.body["offset" + name], elem.documentElement["offset" + name] + ); + + // Get or set width or height on the element + } else if ( size === undefined ) { + var orig = jQuery.css( elem, type ), + ret = parseFloat( orig ); + + return jQuery.isNumeric( ret ) ? ret : orig; + + // Set the width or height on the element (default to pixels if value is unitless) + } else { + return this.css( type, typeof size === "string" ? size : size + "px" ); + } + }; + +}); + + + + +// Expose jQuery to the global object +window.jQuery = window.$ = jQuery; + +// Expose jQuery as an AMD module, but only for AMD loaders that +// understand the issues with loading multiple versions of jQuery +// in a page that all might call define(). The loader will indicate +// they have special allowances for multiple jQuery versions by +// specifying define.amd.jQuery = true. Register as a named module, +// since jQuery can be concatenated with other files that may use define, +// but not use a proper concatenation script that understands anonymous +// AMD modules. A named AMD is safest and most robust way to register. +// Lowercase jquery is used because AMD module names are derived from +// file names, and jQuery is normally delivered in a lowercase file name. +// Do this after creating the global so that if an AMD module wants to call +// noConflict to hide this version of jQuery, it will work. +if ( typeof define === "function" && define.amd && define.amd.jQuery ) { + define( "jquery", [], function () { return jQuery; } ); +} + + + +})( window ); diff --git a/static/js/json2.js b/src/static/js/json2.js index 663f932c..d72b4be9 100644 --- a/static/js/json2.js +++ b/src/static/js/json2.js @@ -465,7 +465,7 @@ if (!JSON) } // If the text is not JSON parseable, then a SyntaxError is thrown. - throw new SyntaxError('JSON.parse'); + throw new SyntaxError('JSON.parse: ' + text); }; } }()); diff --git a/static/js/linestylefilter.js b/src/static/js/linestylefilter.js index 4b455724..1cbfac29 100644 --- a/static/js/linestylefilter.js +++ b/src/static/js/linestylefilter.js @@ -28,11 +28,12 @@ // requires: plugins // requires: undefined -var Changeset = require('/Changeset'); -var plugins = require('/plugins').plugins; -var map = require('/ace2_common').map; - +var Changeset = require('./Changeset'); +var hooks = require('./pluginfw/hooks'); var linestylefilter = {}; +var _ = require('./underscore'); +var AttributeManager = require('./AttributeManager'); + linestylefilter.ATTRIB_CLASSES = { 'bold': 'tag:b', @@ -41,6 +42,9 @@ linestylefilter.ATTRIB_CLASSES = { 'strikethrough': 'tag:s' }; +var lineAttributeMarker = 'lineAttribMarker'; +exports.lineAttributeMarker = lineAttributeMarker; + linestylefilter.getAuthorClassName = function(author) { return "author-" + author.replace(/[^a-y0-9]/g, function(c) @@ -55,8 +59,6 @@ linestylefilter.getAuthorClassName = function(author) linestylefilter.getLineStyleFilter = function(lineLength, aline, textAndClassFunc, apool) { - var plugins_ = plugins; - if (lineLength == 0) return textAndClassFunc; var nextAfterAuthorColors = textAndClassFunc; @@ -71,14 +73,19 @@ linestylefilter.getLineStyleFilter = function(lineLength, aline, textAndClassFun function attribsToClasses(attribs) { var classes = ''; + var isLineAttribMarker = false; + Changeset.eachAttribNumber(attribs, function(n) { - var key = apool.getAttribKey(n); + var key = apool.getAttribKey(n); if (key) { var value = apool.getAttribValue(n); if (value) { + if (!isLineAttribMarker && _.indexOf(AttributeManager.lineAttributes, key) >= 0){ + isLineAttribMarker = true; + } if (key == 'author') { classes += ' ' + linestylefilter.getAuthorClassName(value); @@ -97,15 +104,17 @@ linestylefilter.getLineStyleFilter = function(lineLength, aline, textAndClassFun } else { - classes += plugins_.callHookStr("aceAttribsToClasses", { + classes += hooks.callAllStr("aceAttribsToClasses", { linestylefilter: linestylefilter, key: key, value: value }, " ", " ", ""); - } + } } } }); + + if(isLineAttribMarker) classes += ' ' + lineAttributeMarker; return classes.substring(1); } @@ -300,13 +309,11 @@ linestylefilter.getFilterStack = function(lineText, textAndClassFunc, browser) { var func = linestylefilter.getURLFilter(lineText, textAndClassFunc); - var plugins_ = plugins; - - var hookFilters = plugins_.callHook("aceGetFilterStack", { + var hookFilters = hooks.callAll("aceGetFilterStack", { linestylefilter: linestylefilter, browser: browser }); - map(hookFilters, function(hookFilter) + _.map(hookFilters ,function(hookFilter) { func = hookFilter(lineText, func); }); diff --git a/static/js/pad.js b/src/static/js/pad.js index bda68956..3a1c1633 100644 --- a/static/js/pad.js +++ b/src/static/js/pad.js @@ -26,29 +26,72 @@ var socket; // These jQuery things should create local references, but for now `require()` // assigns to the global `$` and augments it with plugins. -require('/jquery'); -require('/farbtastic'); -require('/excanvas'); -JSON = require('/json2'); -require('/undo-xpopup'); -require('/prefixfree'); - -var chat = require('/chat').chat; -var getCollabClient = require('/collab_client').getCollabClient; -var padconnectionstatus = require('/pad_connectionstatus').padconnectionstatus; -var padcookie = require('/pad_cookie').padcookie; -var paddocbar = require('/pad_docbar').paddocbar; -var padeditbar = require('/pad_editbar').padeditbar; -var padeditor = require('/pad_editor').padeditor; -var padimpexp = require('/pad_impexp').padimpexp; -var padmodals = require('/pad_modals').padmodals; -var padsavedrevs = require('/pad_savedrevs').padsavedrevs; -var paduserlist = require('/pad_userlist').paduserlist; -var padutils = require('/pad_utils').padutils; - -var createCookie = require('/pad_utils').createCookie; -var readCookie = require('/pad_utils').readCookie; -var randomString = require('/pad_utils').randomString; +require('./jquery'); +require('./farbtastic'); +require('./excanvas'); +JSON = require('./json2'); +require('./undo-xpopup'); + +var chat = require('./chat').chat; +var getCollabClient = require('./collab_client').getCollabClient; +var padconnectionstatus = require('./pad_connectionstatus').padconnectionstatus; +var padcookie = require('./pad_cookie').padcookie; +var paddocbar = require('./pad_docbar').paddocbar; +var padeditbar = require('./pad_editbar').padeditbar; +var padeditor = require('./pad_editor').padeditor; +var padimpexp = require('./pad_impexp').padimpexp; +var padmodals = require('./pad_modals').padmodals; +var padsavedrevs = require('./pad_savedrevs'); +var paduserlist = require('./pad_userlist').paduserlist; +var padutils = require('./pad_utils').padutils; + +var createCookie = require('./pad_utils').createCookie; +var readCookie = require('./pad_utils').readCookie; +var randomString = require('./pad_utils').randomString; + +var hooks = require('./pluginfw/hooks'); + +function createCookie(name, value, days, path) +{ + if (days) + { + var date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + var expires = "; expires=" + date.toGMTString(); + } + else var expires = ""; + + if(!path) + path = "/"; + + document.cookie = name + "=" + value + expires + "; path=" + path; +} + +function readCookie(name) +{ + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for (var i = 0; i < ca.length; i++) + { + var c = ca[i]; + while (c.charAt(0) == ' ') c = c.substring(1, c.length); + if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length); + } + return null; +} + +function randomString() +{ + var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + var string_length = 20; + var randomstring = ''; + for (var i = 0; i < string_length; i++) + { + var rnum = Math.floor(Math.random() * chars.length); + randomstring += chars.substring(rnum, rnum + 1); + } + return "t." + randomstring; +} function getParams() { @@ -457,7 +500,7 @@ var pad = { guestPolicy: pad.padOptions.guestPolicy }, this); padimpexp.init(this); - padsavedrevs.init(clientVars.initialRevisionList, this); + padsavedrevs.init(this); padeditor.init(postAceInit, pad.padOptions.view || {}, this); @@ -491,6 +534,7 @@ var pad = { if(padcookie.getPref("showAuthorshipColors") == false){ pad.changeViewOption('showAuthorColors', false); } + hooks.aCallAll("postAceInit", {ace: padeditor.ace}); } }, dispose: function() diff --git a/static/js/pad_connectionstatus.js b/src/static/js/pad_connectionstatus.js index 1de024e8..bb0f0521 100644 --- a/static/js/pad_connectionstatus.js +++ b/src/static/js/pad_connectionstatus.js @@ -20,7 +20,7 @@ * limitations under the License. */ -var padmodals = require('/pad_modals').padmodals; +var padmodals = require('./pad_modals').padmodals; var padconnectionstatus = (function() { diff --git a/static/js/pad_cookie.js b/src/static/js/pad_cookie.js index 1bb5700a..1bb5700a 100644 --- a/static/js/pad_cookie.js +++ b/src/static/js/pad_cookie.js diff --git a/static/js/pad_docbar.js b/src/static/js/pad_docbar.js index b83bf3bf..c5858126 100644 --- a/static/js/pad_docbar.js +++ b/src/static/js/pad_docbar.js @@ -20,7 +20,7 @@ * limitations under the License. */ -var padutils = require('/pad_utils').padutils; +var padutils = require('./pad_utils').padutils; var paddocbar = (function() { @@ -449,7 +449,7 @@ var paddocbar = (function() handleResizePage: function() { // Side-step circular reference. This should be injected. - var padsavedrevs = require('/pad_savedrevs').padsavedrevs; + var padsavedrevs = require('./pad_savedrevs'); padsavedrevs.handleResizePage(); }, hideLaterIfNoOtherInteraction: function() diff --git a/static/js/pad_editbar.js b/src/static/js/pad_editbar.js index 23692631..95d19505 100644 --- a/static/js/pad_editbar.js +++ b/src/static/js/pad_editbar.js @@ -20,9 +20,9 @@ * limitations under the License. */ -var padutils = require('/pad_utils').padutils; -var padeditor = require('/pad_editor').padeditor; -var padsavedrevs = require('/pad_savedrevs').padsavedrevs; +var padutils = require('./pad_utils').padutils; +var padeditor = require('./pad_editor').padeditor; +var padsavedrevs = require('./pad_savedrevs'); function indexOf(array, value) { for (var i = 0, ii = array.length; i < ii; i++) { @@ -131,7 +131,7 @@ var padeditbar = (function() { self.toogleDropDown("importexport"); } - else if (cmd == 'save') + else if (cmd == 'savedRevision') { padsavedrevs.saveNow(); } @@ -200,10 +200,11 @@ var padeditbar = (function() else { var nth_child = indexOf(modules, moduleName) + 1; - if (nth_child > 0 && nth_child <= 3) { + if (nth_child > 0 && nth_child <= (modules.length-1)) { $("#editbar ul#menu_right li:not(:nth-child(" + nth_child + "))").removeClass("selected"); $("#editbar ul#menu_right li:nth-child(" + nth_child + ")").toggleClass("selected"); } + if(modules[modules.length-1] === moduleName) $("#editbar ul#menu_right li").removeClass("selected"); //hide all modules that are not selected and show the selected one for(var i=0;i<modules.length;i++) { @@ -237,14 +238,14 @@ var padeditbar = (function() { var basePath = document.location.href.substring(0, document.location.href.indexOf("/p/")); var readonlyLink = basePath + "/ro/" + clientVars.readOnlyId; - $('#embedinput').val("<iframe src='" + readonlyLink + "?showControls=true&showChat=true&showLineNumbers=true&useMonospaceFont=false' width=600 height=400>"); + $('#embedinput').val("<iframe name='embed_readonly' src='" + readonlyLink + "?showControls=true&showChat=true&showLineNumbers=true&useMonospaceFont=false' width=600 height=400>"); $('#linkinput').val(readonlyLink); $('#embedreadonlyqr').attr("src","https://chart.googleapis.com/chart?chs=200x200&cht=qr&chld=|0&chl=" + readonlyLink); } else { var padurl = window.location.href.split("?")[0]; - $('#embedinput').val("<iframe src='" + padurl + "?showControls=true&showChat=true&showLineNumbers=true&useMonospaceFont=false' width=600 height=400>"); + $('#embedinput').val("<iframe name='embed_readwrite' src='" + padurl + "?showControls=true&showChat=true&showLineNumbers=true&useMonospaceFont=false' width=600 height=400>"); $('#linkinput').val(padurl); $('#embedreadonlyqr').attr("src","https://chart.googleapis.com/chart?chs=200x200&cht=qr&chld=|0&chl=" + padurl); } diff --git a/static/js/pad_editor.js b/src/static/js/pad_editor.js index 3dd67544..b8a4ea0e 100644 --- a/static/js/pad_editor.js +++ b/src/static/js/pad_editor.js @@ -20,8 +20,8 @@ * limitations under the License. */ -var padcookie = require('/pad_cookie').padcookie; -var padutils = require('/pad_utils').padutils; +var padcookie = require('./pad_cookie').padcookie; +var padutils = require('./pad_utils').padutils; var padeditor = (function() { @@ -34,7 +34,7 @@ var padeditor = (function() viewZoom: 100, init: function(readyFunc, initialViewOptions, _pad) { - Ace2Editor = require('/ace').Ace2Editor; + Ace2Editor = require('./ace').Ace2Editor; pad = _pad; settings = pad.settings; @@ -87,8 +87,6 @@ var padeditor = (function() return defaultValue; } - self.ace.setProperty("showsauthorcolors", !settings.noColors); - self.ace.setProperty("rtlIsTrue", settings.rtlIsTrue); var v; @@ -100,6 +98,8 @@ var padeditor = (function() v = getOption('showAuthorColors', true); self.ace.setProperty("showsauthorcolors", v); padutils.setCheckbox($("#options-colorscheck"), v); + // Override from parameters + self.ace.setProperty("showsauthorcolors", !settings.noColors); v = getOption('useMonospaceFont', false); self.ace.setProperty("textface", (v ? "monospace" : "Arial, sans-serif")); diff --git a/static/js/pad_impexp.js b/src/static/js/pad_impexp.js index 9ff09d19..23655ba4 100644 --- a/static/js/pad_impexp.js +++ b/src/static/js/pad_impexp.js @@ -20,7 +20,7 @@ * limitations under the License. */ -var paddocbar = require('/pad_docbar').paddocbar; +var paddocbar = require('./pad_docbar').paddocbar; var padimpexp = (function() { diff --git a/static/js/pad_modals.js b/src/static/js/pad_modals.js index b78e28f4..0dd281bb 100644 --- a/static/js/pad_modals.js +++ b/src/static/js/pad_modals.js @@ -20,8 +20,8 @@ * limitations under the License. */ -var padutils = require('/pad_utils').padutils; -var paddocbar = require('/pad_docbar').paddocbar; +var padutils = require('./pad_utils').padutils; +var paddocbar = require('./pad_docbar').paddocbar; var padmodals = (function() { diff --git a/node/utils/common_code.js b/src/static/js/pad_savedrevs.js index 645a74ba..bf4f941c 100644 --- a/node/utils/common_code.js +++ b/src/static/js/pad_savedrevs.js @@ -1,5 +1,5 @@ /** - * Copyright 2009 Google Inc. + * Copyright 2012 Peter 'Pita' Martischka * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,11 +14,13 @@ * limitations under the License. */ -var RequireKernel = require('require-kernel/'); -var path = require('path'); +var pad; -var CLIENT_JS_SRC = path.normalize(__dirname + '/../../static/js/'); -var client_require = RequireKernel.requireForPaths( - 'file://' + (CLIENT_JS_SRC.charAt(0) == '/' ? '' : '/') + encodeURI(CLIENT_JS_SRC)); +exports.saveNow = function(){ + pad.collabClient.sendMessage({"type": "SAVE_REVISION"}); + alert("This revision is now marked as a saved revision"); +} -exports.require = client_require; +exports.init = function(_pad){ + pad = _pad; +} diff --git a/static/js/pad_userlist.js b/src/static/js/pad_userlist.js index 2c063d74..5a3f9b35 100644 --- a/static/js/pad_userlist.js +++ b/src/static/js/pad_userlist.js @@ -20,7 +20,7 @@ * limitations under the License. */ -var padutils = require('/pad_utils').padutils; +var padutils = require('./pad_utils').padutils; var myUserInfo = {}; diff --git a/static/js/pad_utils.js b/src/static/js/pad_utils.js index 499f7126..83ee9aae 100644 --- a/static/js/pad_utils.js +++ b/src/static/js/pad_utils.js @@ -20,7 +20,7 @@ * limitations under the License. */ -var Security = require('/security'); +var Security = require('./security'); /** * Generates a random String with the given length. Is needed to generate the Author, Group, readonly, session Ids @@ -75,7 +75,7 @@ var padutils = { }, uniqueId: function() { - var pad = require('/pad').pad; // Sidestep circular dependency + var pad = require('./pad').pad; // Sidestep circular dependency function encodeNum(n, width) { // returns string that is exactly 'width' chars, padding with zeros @@ -250,7 +250,7 @@ var padutils = { }, timediff: function(d) { - var pad = require('/pad').pad; // Sidestep circular dependency + var pad = require('./pad').pad; // Sidestep circular dependency function format(n, word) { n = Math.round(n); @@ -529,7 +529,7 @@ function setupGlobalExceptionHandler() { padutils.setupGlobalExceptionHandler = setupGlobalExceptionHandler; -padutils.binarySearch = require('/ace2_common').binarySearch; +padutils.binarySearch = require('./ace2_common').binarySearch; exports.randomString = randomString; exports.createCookie = createCookie; diff --git a/src/static/js/pluginfw/async.js b/src/static/js/pluginfw/async.js new file mode 100644 index 00000000..c862008a --- /dev/null +++ b/src/static/js/pluginfw/async.js @@ -0,0 +1,690 @@ +/*global setTimeout: false, console: false */ +(function () { + + var async = {}; + + // global on the server, window in the browser + var root = this, + previous_async = root.async; + + if (typeof module !== 'undefined' && module.exports) { + module.exports = async; + } + else { + root.async = async; + } + + async.noConflict = function () { + root.async = previous_async; + return async; + }; + + //// cross-browser compatiblity functions //// + + var _forEach = function (arr, iterator) { + if (arr.forEach) { + return arr.forEach(iterator); + } + for (var i = 0; i < arr.length; i += 1) { + iterator(arr[i], i, arr); + } + }; + + var _map = function (arr, iterator) { + if (arr.map) { + return arr.map(iterator); + } + var results = []; + _forEach(arr, function (x, i, a) { + results.push(iterator(x, i, a)); + }); + return results; + }; + + var _reduce = function (arr, iterator, memo) { + if (arr.reduce) { + return arr.reduce(iterator, memo); + } + _forEach(arr, function (x, i, a) { + memo = iterator(memo, x, i, a); + }); + return memo; + }; + + var _keys = function (obj) { + if (Object.keys) { + return Object.keys(obj); + } + var keys = []; + for (var k in obj) { + if (obj.hasOwnProperty(k)) { + keys.push(k); + } + } + return keys; + }; + + var _indexOf = function (arr, item) { + if (arr.indexOf) { + return arr.indexOf(item); + } + for (var i = 0; i < arr.length; i += 1) { + if (arr[i] === item) { + return i; + } + } + return -1; + }; + + //// exported async module functions //// + + //// nextTick implementation with browser-compatible fallback //// + if (typeof process === 'undefined' || !(process.nextTick)) { + async.nextTick = function (fn) { + setTimeout(fn, 0); + }; + } + else { + async.nextTick = process.nextTick; + } + + async.forEach = function (arr, iterator, callback) { + if (!arr.length) { + return callback(); + } + var completed = 0; + _forEach(arr, function (x) { + iterator(x, function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + if (completed === arr.length) { + callback(); + } + } + }); + }); + }; + + async.forEachSeries = function (arr, iterator, callback) { + if (!arr.length) { + return callback(); + } + var completed = 0; + var iterate = function () { + iterator(arr[completed], function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + if (completed === arr.length) { + callback(); + } + else { + iterate(); + } + } + }); + }; + iterate(); + }; + + async.forEachLimit = function (arr, limit, iterator, callback) { + if (!arr.length || limit <= 0) { + return callback(); + } + var completed = 0; + var started = 0; + var running = 0; + + (function replenish () { + if (completed === arr.length) { + return callback(); + } + + while (running < limit && started < arr.length) { + iterator(arr[started], function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + running -= 1; + if (completed === arr.length) { + callback(); + } + else { + replenish(); + } + } + }); + started += 1; + running += 1; + } + })(); + }; + + + var doParallel = function (fn) { + return function () { + var args = Array.prototype.slice.call(arguments); + return fn.apply(null, [async.forEach].concat(args)); + }; + }; + var doSeries = function (fn) { + return function () { + var args = Array.prototype.slice.call(arguments); + return fn.apply(null, [async.forEachSeries].concat(args)); + }; + }; + + + var _asyncMap = function (eachfn, arr, iterator, callback) { + var results = []; + arr = _map(arr, function (x, i) { + return {index: i, value: x}; + }); + eachfn(arr, function (x, callback) { + iterator(x.value, function (err, v) { + results[x.index] = v; + callback(err); + }); + }, function (err) { + callback(err, results); + }); + }; + async.map = doParallel(_asyncMap); + async.mapSeries = doSeries(_asyncMap); + + + // reduce only has a series version, as doing reduce in parallel won't + // work in many situations. + async.reduce = function (arr, memo, iterator, callback) { + async.forEachSeries(arr, function (x, callback) { + iterator(memo, x, function (err, v) { + memo = v; + callback(err); + }); + }, function (err) { + callback(err, memo); + }); + }; + // inject alias + async.inject = async.reduce; + // foldl alias + async.foldl = async.reduce; + + async.reduceRight = function (arr, memo, iterator, callback) { + var reversed = _map(arr, function (x) { + return x; + }).reverse(); + async.reduce(reversed, memo, iterator, callback); + }; + // foldr alias + async.foldr = async.reduceRight; + + var _filter = function (eachfn, arr, iterator, callback) { + var results = []; + arr = _map(arr, function (x, i) { + return {index: i, value: x}; + }); + eachfn(arr, function (x, callback) { + iterator(x.value, function (v) { + if (v) { + results.push(x); + } + callback(); + }); + }, function (err) { + callback(_map(results.sort(function (a, b) { + return a.index - b.index; + }), function (x) { + return x.value; + })); + }); + }; + async.filter = doParallel(_filter); + async.filterSeries = doSeries(_filter); + // select alias + async.select = async.filter; + async.selectSeries = async.filterSeries; + + var _reject = function (eachfn, arr, iterator, callback) { + var results = []; + arr = _map(arr, function (x, i) { + return {index: i, value: x}; + }); + eachfn(arr, function (x, callback) { + iterator(x.value, function (v) { + if (!v) { + results.push(x); + } + callback(); + }); + }, function (err) { + callback(_map(results.sort(function (a, b) { + return a.index - b.index; + }), function (x) { + return x.value; + })); + }); + }; + async.reject = doParallel(_reject); + async.rejectSeries = doSeries(_reject); + + var _detect = function (eachfn, arr, iterator, main_callback) { + eachfn(arr, function (x, callback) { + iterator(x, function (result) { + if (result) { + main_callback(x); + main_callback = function () {}; + } + else { + callback(); + } + }); + }, function (err) { + main_callback(); + }); + }; + async.detect = doParallel(_detect); + async.detectSeries = doSeries(_detect); + + async.some = function (arr, iterator, main_callback) { + async.forEach(arr, function (x, callback) { + iterator(x, function (v) { + if (v) { + main_callback(true); + main_callback = function () {}; + } + callback(); + }); + }, function (err) { + main_callback(false); + }); + }; + // any alias + async.any = async.some; + + async.every = function (arr, iterator, main_callback) { + async.forEach(arr, function (x, callback) { + iterator(x, function (v) { + if (!v) { + main_callback(false); + main_callback = function () {}; + } + callback(); + }); + }, function (err) { + main_callback(true); + }); + }; + // all alias + async.all = async.every; + + async.sortBy = function (arr, iterator, callback) { + async.map(arr, function (x, callback) { + iterator(x, function (err, criteria) { + if (err) { + callback(err); + } + else { + callback(null, {value: x, criteria: criteria}); + } + }); + }, function (err, results) { + if (err) { + return callback(err); + } + else { + var fn = function (left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }; + callback(null, _map(results.sort(fn), function (x) { + return x.value; + })); + } + }); + }; + + async.auto = function (tasks, callback) { + callback = callback || function () {}; + var keys = _keys(tasks); + if (!keys.length) { + return callback(null); + } + + var results = {}; + + var listeners = []; + var addListener = function (fn) { + listeners.unshift(fn); + }; + var removeListener = function (fn) { + for (var i = 0; i < listeners.length; i += 1) { + if (listeners[i] === fn) { + listeners.splice(i, 1); + return; + } + } + }; + var taskComplete = function () { + _forEach(listeners, function (fn) { + fn(); + }); + }; + + addListener(function () { + if (_keys(results).length === keys.length) { + callback(null, results); + } + }); + + _forEach(keys, function (k) { + var task = (tasks[k] instanceof Function) ? [tasks[k]]: tasks[k]; + var taskCallback = function (err) { + if (err) { + callback(err); + // stop subsequent errors hitting callback multiple times + callback = function () {}; + } + else { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + results[k] = args; + taskComplete(); + } + }; + var requires = task.slice(0, Math.abs(task.length - 1)) || []; + var ready = function () { + return _reduce(requires, function (a, x) { + return (a && results.hasOwnProperty(x)); + }, true); + }; + if (ready()) { + task[task.length - 1](taskCallback, results); + } + else { + var listener = function () { + if (ready()) { + removeListener(listener); + task[task.length - 1](taskCallback, results); + } + }; + addListener(listener); + } + }); + }; + + async.waterfall = function (tasks, callback) { + if (!tasks.length) { + return callback(); + } + callback = callback || function () {}; + var wrapIterator = function (iterator) { + return function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + var args = Array.prototype.slice.call(arguments, 1); + var next = iterator.next(); + if (next) { + args.push(wrapIterator(next)); + } + else { + args.push(callback); + } + async.nextTick(function () { + iterator.apply(null, args); + }); + } + }; + }; + wrapIterator(async.iterator(tasks))(); + }; + + async.parallel = function (tasks, callback) { + callback = callback || function () {}; + if (tasks.constructor === Array) { + async.map(tasks, function (fn, callback) { + if (fn) { + fn(function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + callback.call(null, err, args); + }); + } + }, callback); + } + else { + var results = {}; + async.forEach(_keys(tasks), function (k, callback) { + tasks[k](function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + results[k] = args; + callback(err); + }); + }, function (err) { + callback(err, results); + }); + } + }; + + async.series = function (tasks, callback) { + callback = callback || function () {}; + if (tasks.constructor === Array) { + async.mapSeries(tasks, function (fn, callback) { + if (fn) { + fn(function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + callback.call(null, err, args); + }); + } + }, callback); + } + else { + var results = {}; + async.forEachSeries(_keys(tasks), function (k, callback) { + tasks[k](function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + results[k] = args; + callback(err); + }); + }, function (err) { + callback(err, results); + }); + } + }; + + async.iterator = function (tasks) { + var makeCallback = function (index) { + var fn = function () { + if (tasks.length) { + tasks[index].apply(null, arguments); + } + return fn.next(); + }; + fn.next = function () { + return (index < tasks.length - 1) ? makeCallback(index + 1): null; + }; + return fn; + }; + return makeCallback(0); + }; + + async.apply = function (fn) { + var args = Array.prototype.slice.call(arguments, 1); + return function () { + return fn.apply( + null, args.concat(Array.prototype.slice.call(arguments)) + ); + }; + }; + + var _concat = function (eachfn, arr, fn, callback) { + var r = []; + eachfn(arr, function (x, cb) { + fn(x, function (err, y) { + r = r.concat(y || []); + cb(err); + }); + }, function (err) { + callback(err, r); + }); + }; + async.concat = doParallel(_concat); + async.concatSeries = doSeries(_concat); + + async.whilst = function (test, iterator, callback) { + if (test()) { + iterator(function (err) { + if (err) { + return callback(err); + } + async.whilst(test, iterator, callback); + }); + } + else { + callback(); + } + }; + + async.until = function (test, iterator, callback) { + if (!test()) { + iterator(function (err) { + if (err) { + return callback(err); + } + async.until(test, iterator, callback); + }); + } + else { + callback(); + } + }; + + async.queue = function (worker, concurrency) { + var workers = 0; + var q = { + tasks: [], + concurrency: concurrency, + saturated: null, + empty: null, + drain: null, + push: function (data, callback) { + q.tasks.push({data: data, callback: callback}); + if(q.saturated && q.tasks.length == concurrency) q.saturated(); + async.nextTick(q.process); + }, + process: function () { + if (workers < q.concurrency && q.tasks.length) { + var task = q.tasks.shift(); + if(q.empty && q.tasks.length == 0) q.empty(); + workers += 1; + worker(task.data, function () { + workers -= 1; + if (task.callback) { + task.callback.apply(task, arguments); + } + if(q.drain && q.tasks.length + workers == 0) q.drain(); + q.process(); + }); + } + }, + length: function () { + return q.tasks.length; + }, + running: function () { + return workers; + } + }; + return q; + }; + + var _console_fn = function (name) { + return function (fn) { + var args = Array.prototype.slice.call(arguments, 1); + fn.apply(null, args.concat([function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (typeof console !== 'undefined') { + if (err) { + if (console.error) { + console.error(err); + } + } + else if (console[name]) { + _forEach(args, function (x) { + console[name](x); + }); + } + } + }])); + }; + }; + async.log = _console_fn('log'); + async.dir = _console_fn('dir'); + /*async.info = _console_fn('info'); + async.warn = _console_fn('warn'); + async.error = _console_fn('error');*/ + + async.memoize = function (fn, hasher) { + var memo = {}; + var queues = {}; + hasher = hasher || function (x) { + return x; + }; + var memoized = function () { + var args = Array.prototype.slice.call(arguments); + var callback = args.pop(); + var key = hasher.apply(null, args); + if (key in memo) { + callback.apply(null, memo[key]); + } + else if (key in queues) { + queues[key].push(callback); + } + else { + queues[key] = [callback]; + fn.apply(null, args.concat([function () { + memo[key] = arguments; + var q = queues[key]; + delete queues[key]; + for (var i = 0, l = q.length; i < l; i++) { + q[i].apply(null, arguments); + } + }])); + } + }; + memoized.unmemoized = fn; + return memoized; + }; + + async.unmemoize = function (fn) { + return function () { + return (fn.unmemoized || fn).apply(null, arguments); + } + }; + +}()); diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js new file mode 100644 index 00000000..c4cd5aeb --- /dev/null +++ b/src/static/js/pluginfw/hooks.js @@ -0,0 +1,90 @@ +var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins"); +var _; + +/* FIXME: Ugly hack, in the future, use same code for server & client */ +if (plugins.isClient) { + var async = require("ep_etherpad-lite/static/js/pluginfw/async"); + _ = require("ep_etherpad-lite/static/js/underscore"); +} else { + var async = require("async"); + _ = require("underscore"); +} + +exports.bubbleExceptions = true + +var hookCallWrapper = function (hook, hook_name, args, cb) { + if (cb === undefined) cb = function (x) { return x; }; + if (exports.bubbleExceptions) { + return hook.hook_fn(hook_name, args, cb); + } else { + try { + return hook.hook_fn(hook_name, args, cb); + } catch (ex) { + console.error([hook_name, hook.part.full_name, ex.stack || ex]); + } + } +} + + +/* Don't use Array.concat as it flatterns arrays within the array */ +exports.flatten = function (lst) { + var res = []; + if (lst != undefined && lst != null) { + for (var i = 0; i < lst.length; i++) { + if (lst[i] != undefined && lst[i] != null) { + for (var j = 0; j < lst[i].length; j++) { + res.push(lst[i][j]); + } + } + } + } + return res; +} + +exports.callAll = function (hook_name, args) { + if (!args) args = {}; + if (plugins.hooks[hook_name] === undefined) return []; + return exports.flatten(_.map(plugins.hooks[hook_name], function (hook) { + return hookCallWrapper(hook, hook_name, args); + })); +} + +exports.aCallAll = function (hook_name, args, cb) { + if (!args) args = {}; + if (!cb) cb = function () {}; + if (plugins.hooks[hook_name] === undefined) return cb(null, []); + async.map( + plugins.hooks[hook_name], + function (hook, cb) { + hookCallWrapper(hook, hook_name, args, function (res) { cb(null, res); }); + }, + function (err, res) { + cb(null, exports.flatten(res)); + } + ); +} + +exports.callFirst = function (hook_name, args) { + if (!args) args = {}; + if (plugins.hooks[hook_name][0] === undefined) return []; + return exports.flatten(hookCallWrapper(plugins.hooks[hook_name][0], hook_name, args)); +} + +exports.aCallFirst = function (hook_name, args, cb) { + if (!args) args = {}; + if (!cb) cb = function () {}; + if (plugins.hooks[hook_name][0] === undefined) return cb(null, []); + hookCallWrapper(plugins.hooks[hook_name][0], hook_name, args, function (res) { cb(null, exports.flatten(res)); }); +} + +exports.callAllStr = function(hook_name, args, sep, pre, post) { + if (sep == undefined) sep = ''; + if (pre == undefined) pre = ''; + if (post == undefined) post = ''; + var newCallhooks = []; + var callhooks = exports.callAll(hook_name, args); + for (var i = 0, ii = callhooks.length; i < ii; i++) { + newCallhooks[i] = pre + callhooks[i] + post; + } + return newCallhooks.join(sep || ""); +} diff --git a/src/static/js/pluginfw/installer.js b/src/static/js/pluginfw/installer.js new file mode 100644 index 00000000..127a95aa --- /dev/null +++ b/src/static/js/pluginfw/installer.js @@ -0,0 +1,76 @@ +var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins"); +var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); +var npm = require("npm"); +var registry = require("npm/lib/utils/npm-registry-client/index.js"); + +var withNpm = function (npmfn, cb) { + npm.load({}, function (er) { + if (er) return cb({progress:1, error:er}); + npm.on("log", function (message) { + cb({progress: 0.5, message:message.msg + ": " + message.pref}); + }); + npmfn(function (er, data) { + if (er) return cb({progress:1, error:er.code + ": " + er.path}); + if (!data) data = {}; + data.progress = 1; + data.message = "Done."; + cb(data); + }); + }); +} + +// All these functions call their callback multiple times with +// {progress:[0,1], message:STRING, error:object}. They will call it +// with progress = 1 at least once, and at all times will either +// message or error be present, not both. It can be called multiple +// times for all values of propgress except for 1. + +exports.uninstall = function(plugin_name, cb) { + withNpm( + function (cb) { + npm.commands.uninstall([plugin_name], function (er) { + if (er) return cb(er); + hooks.aCallAll("pluginUninstall", {plugin_name: plugin_name}, function (er, data) { + if (er) return cb(er); + plugins.update(cb); + }); + }); + }, + cb + ); +}; + +exports.install = function(plugin_name, cb) { + withNpm( + function (cb) { + npm.commands.install([plugin_name], function (er) { + if (er) return cb(er); + hooks.aCallAll("pluginInstall", {plugin_name: plugin_name}, function (er, data) { + if (er) return cb(er); + plugins.update(cb); + }); + }); + }, + cb + ); +}; + +exports.search = function(pattern, cb) { + withNpm( + function (cb) { + registry.get( + "/-/all", null, 600, false, true, + function (er, data) { + if (er) return cb(er); + var res = {}; + for (key in data) { + if (key.indexOf(plugins.prefix) == 0 && key.indexOf(pattern) != -1) + res[key] = data[key]; + } + cb(null, {results:res}); + } + ); + }, + cb + ); +}; diff --git a/src/static/js/pluginfw/parent_require.js b/src/static/js/pluginfw/parent_require.js new file mode 100644 index 00000000..d7f6190d --- /dev/null +++ b/src/static/js/pluginfw/parent_require.js @@ -0,0 +1,37 @@ +/** + * This module allows passing require modules instances to + * embedded iframes in a page. + * For example, if a page has the "plugins" module initialized, + * it is important to use exactly the same "plugins" instance + * inside iframes as well. Otherwise, plugins cannot save any + * state. + */ + + +/** + * Instructs the require object that when a reqModuleName module + * needs to be loaded, that it iterates through the parents of the + * current window until it finds one who can execute "require" + * statements and asks it to perform require on reqModuleName. + * + * @params requireDefObj Require object which supports define + * statements. This object is accessible after loading require-kernel. + * @params reqModuleName Module name e.g. (ep_etherpad-lite/static/js/plugins) + */ +exports.getRequirementFromParent = function(requireDefObj, reqModuleName) { + requireDefObj.define(reqModuleName, function(require, exports, module) { + var t = parent; + var max = 0; // make sure I don't go up more than 10 times + while (typeof(t) != "undefined") { + max++; + if (max==10) + break; + if (typeof(t.require) != "undefined") { + module.exports = t.require(reqModuleName); + return; + } + t = t.parent; + } + }); + +} diff --git a/src/static/js/pluginfw/plugins.js b/src/static/js/pluginfw/plugins.js new file mode 100644 index 00000000..5f44bb6f --- /dev/null +++ b/src/static/js/pluginfw/plugins.js @@ -0,0 +1,226 @@ +exports.isClient = typeof global != "object"; + +var _; + +if (!exports.isClient) { + var npm = require("npm/lib/npm.js"); + var readInstalled = require("./read-installed.js"); + var relativize = require("npm/lib/utils/relativize.js"); + var readJson = require("npm/lib/utils/read-json.js"); + var path = require("path"); + var async = require("async"); + var fs = require("fs"); + var tsort = require("./tsort"); + var util = require("util"); + _ = require("underscore"); +}else{ + var $, jQuery; + $ = jQuery = require("ep_etherpad-lite/static/js/rjquery").$; + _ = require("ep_etherpad-lite/static/js/underscore"); +} + +exports.prefix = 'ep_'; +exports.loaded = false; +exports.plugins = {}; +exports.parts = []; +exports.hooks = {}; + +exports.ensure = function (cb) { + if (!exports.loaded) + exports.update(cb); + else + cb(); +}; + +exports.formatPlugins = function () { + return _.keys(exports.plugins).join(", "); +}; + +exports.formatParts = function () { + return _.map(exports.parts, function (part) { return part.full_name; }).join("\n"); +}; + +exports.formatHooks = function () { + var res = []; + _.chain(exports.hooks).keys().forEach(function (hook_name) { + _.forEach(exports.hooks[hook_name], function (hook) { + res.push(hook.hook_name + ": " + hook.hook_fn_name + " from " + hook.part.full_name); + }); + }); + return res.join("\n"); +}; + +exports.loadFn = function (path, hookName) { + var x = path.split(":"); + var fn = require(x[0]); + var functionName = x[1] ? x[1] : hookName; + + _.each(functionName.split("."), function (name) { + fn = fn[name]; + }); + return fn; +}; + +exports.extractHooks = function (parts, hook_set_name) { + var hooks = {}; + _.each(parts,function (part) { + _.chain(part[hook_set_name] || {}) + .keys() + .each(function (hook_name) { + if (hooks[hook_name] === undefined) hooks[hook_name] = []; + + + var hook_fn_name = part[hook_set_name][hook_name]; + var hook_fn = exports.loadFn(hook_fn_name, hook_name); + if (hook_fn) { + hooks[hook_name].push({"hook_name": hook_name, "hook_fn": hook_fn, "hook_fn_name": hook_fn_name, "part": part}); + } else { + console.error("Unable to load hook function for " + part.full_name + " for hook " + hook_name + ": " + part.hooks[hook_name]); + } + }); + }); + return hooks; +}; + + +if (exports.isClient) { + exports.update = function (cb) { + jQuery.getJSON('/pluginfw/plugin-definitions.json', function(data) { + exports.plugins = data.plugins; + exports.parts = data.parts; + exports.hooks = exports.extractHooks(exports.parts, "client_hooks"); + exports.loaded = true; + cb(); + }).error(function(xhr, s, err){ + console.error("Failed to load plugin-definitions: " + err); + cb(); + }); + }; +} else { + +exports.callInit = function (cb) { + var hooks = require("./hooks"); + async.map( + Object.keys(exports.plugins), + function (plugin_name, cb) { + var plugin = exports.plugins[plugin_name]; + fs.stat(path.normalize(path.join(plugin.package.path, ".ep_initialized")), function (err, stats) { + if (err) { + async.waterfall([ + function (cb) { fs.writeFile(path.normalize(path.join(plugin.package.path, ".ep_initialized")), 'done', cb); }, + function (cb) { hooks.aCallAll("init_" + plugin_name, {}, cb); }, + cb, + ]); + } else { + cb(); + } + }); + }, + function () { cb(); } + ); +} + +exports.update = function (cb) { + exports.getPackages(function (er, packages) { + var parts = []; + var plugins = {}; + // Load plugin metadata ep.json + async.forEach( + Object.keys(packages), + function (plugin_name, cb) { + exports.loadPlugin(packages, plugin_name, plugins, parts, cb); + }, + function (err) { + if (err) cb(err); + exports.plugins = plugins; + exports.parts = exports.sortParts(parts); + exports.hooks = exports.extractHooks(exports.parts, "hooks"); + exports.loaded = true; + exports.callInit(cb); + } + ); + }); + }; + +exports.getPackages = function (cb) { + // Load list of installed NPM packages, flatten it to a list, and filter out only packages with names that + var dir = path.resolve(npm.dir, '..'); + readInstalled(dir, function (er, data) { + if (er) cb(er, null); + var packages = {}; + function flatten(deps) { + _.chain(deps).keys().each(function (name) { + if (name.indexOf(exports.prefix) === 0) { + packages[name] = _.clone(deps[name]); + // Delete anything that creates loops so that the plugin + // list can be sent as JSON to the web client + delete packages[name].dependencies; + delete packages[name].parent; + } + + if (deps[name].dependencies !== undefined) flatten(deps[name].dependencies); + }); + } + + var tmp = {}; + tmp[data.name] = data; + flatten(tmp); + cb(null, packages); + }); + }; + + exports.loadPlugin = function (packages, plugin_name, plugins, parts, cb) { + var plugin_path = path.resolve(packages[plugin_name].path, "ep.json"); + fs.readFile( + plugin_path, + function (er, data) { + if (er) { + console.error("Unable to load plugin definition file " + plugin_path); + return cb(); + } + try { + var plugin = JSON.parse(data); + plugin['package'] = packages[plugin_name]; + plugins[plugin_name] = plugin; + _.each(plugin.parts, function (part) { + part.plugin = plugin_name; + part.full_name = plugin_name + "/" + part.name; + parts[part.full_name] = part; + }); + } catch (ex) { + console.error("Unable to parse plugin definition file " + plugin_path + ": " + ex.toString()); + } + cb(); + } + ); + }; + +exports.partsToParentChildList = function (parts) { + var res = []; + _.chain(parts).keys().forEach(function (name) { + _.each(parts[name].post || [], function (child_name) { + res.push([name, child_name]); + }); + _.each(parts[name].pre || [], function (parent_name) { + res.push([parent_name, name]); + }); + if (!parts[name].pre && !parts[name].post) { + res.push([name, ":" + name]); // Include apps with no dependency info + } + }); + return res; +}; + + +// Used only in Node, so no need for _ +exports.sortParts = function(parts) { + return tsort( + exports.partsToParentChildList(parts) + ).filter( + function (name) { return parts[name] !== undefined; } + ).map( + function (name) { return parts[name]; } + ); +}; + +}
\ No newline at end of file diff --git a/src/static/js/pluginfw/read-installed.js b/src/static/js/pluginfw/read-installed.js new file mode 100644 index 00000000..cc03b357 --- /dev/null +++ b/src/static/js/pluginfw/read-installed.js @@ -0,0 +1,324 @@ +// A copy of npm/lib/utils/read-installed.js +// that is hacked to not cache everything :) + +// Walk through the file-system "database" of installed +// packages, and create a data object related to the +// installed versions of each package. + +/* +This will traverse through all node_modules folders, +resolving the dependencies object to the object corresponding to +the package that meets that dep, or just the version/range if +unmet. + +Assuming that you had this folder structure: + +/path/to ++-- package.json { name = "root" } +`-- node_modules + +-- foo {bar, baz, asdf} + | +-- node_modules + | +-- bar { baz } + | `-- baz + `-- asdf + +where "foo" depends on bar, baz, and asdf, bar depends on baz, +and bar and baz are bundled with foo, whereas "asdf" is at +the higher level (sibling to foo), you'd get this object structure: + +{ <package.json data> +, path: "/path/to" +, parent: null +, dependencies: + { foo : + { version: "1.2.3" + , path: "/path/to/node_modules/foo" + , parent: <Circular: root> + , dependencies: + { bar: + { parent: <Circular: foo> + , path: "/path/to/node_modules/foo/node_modules/bar" + , version: "2.3.4" + , dependencies: { baz: <Circular: foo.dependencies.baz> } + } + , baz: { ... } + , asdf: <Circular: asdf> + } + } + , asdf: { ... } + } +} + +Unmet deps are left as strings. +Extraneous deps are marked with extraneous:true +deps that don't meet a requirement are marked with invalid:true + +to READ(packagefolder, parentobj, name, reqver) +obj = read package.json +installed = ./node_modules/* +if parentobj is null, and no package.json + obj = {dependencies:{<installed>:"*"}} +deps = Object.keys(obj.dependencies) +obj.path = packagefolder +obj.parent = parentobj +if name, && obj.name !== name, obj.invalid = true +if reqver, && obj.version !satisfies reqver, obj.invalid = true +if !reqver && parentobj, obj.extraneous = true +for each folder in installed + obj.dependencies[folder] = READ(packagefolder+node_modules+folder, + obj, folder, obj.dependencies[folder]) +# walk tree to find unmet deps +for each dep in obj.dependencies not in installed + r = obj.parent + while r + if r.dependencies[dep] + if r.dependencies[dep].verion !satisfies obj.dependencies[dep] + WARN + r.dependencies[dep].invalid = true + obj.dependencies[dep] = r.dependencies[dep] + r = null + else r = r.parent +return obj + + +TODO: +1. Find unmet deps in parent directories, searching as node does up +as far as the left-most node_modules folder. +2. Ignore anything in node_modules that isn't a package folder. + +*/ + + +var npm = require("npm/lib/npm.js") + , fs = require("graceful-fs") + , path = require("path") + , asyncMap = require("slide").asyncMap + , semver = require("semver") + , readJson = require("npm/lib/utils/read-json.js") + , log = require("npm/lib/utils/log.js") + +module.exports = readInstalled + +function readInstalled (folder, cb) { + /* This is where we clear the cache, these three lines are all the + * new code there is */ + rpSeen = {}; + riSeen = []; + var fuSeen = []; + + var d = npm.config.get("depth") + readInstalled_(folder, null, null, null, 0, d, function (er, obj) { + if (er) return cb(er) + // now obj has all the installed things, where they're installed + // figure out the inheritance links, now that the object is built. + resolveInheritance(obj) + cb(null, obj) + }) +} + +var rpSeen = {} +function readInstalled_ (folder, parent, name, reqver, depth, maxDepth, cb) { + //console.error(folder, name) + + var installed + , obj + , real + , link + + fs.readdir(path.resolve(folder, "node_modules"), function (er, i) { + // error indicates that nothing is installed here + if (er) i = [] + installed = i.filter(function (f) { return f.charAt(0) !== "." }) + next() + }) + + readJson(path.resolve(folder, "package.json"), function (er, data) { + obj = copy(data) + + if (!parent) { + obj = obj || true + er = null + } + return next(er) + }) + + fs.lstat(folder, function (er, st) { + if (er) { + if (!parent) real = true + return next(er) + } + fs.realpath(folder, function (er, rp) { + //console.error("realpath(%j) = %j", folder, rp) + real = rp + if (st.isSymbolicLink()) link = rp + next(er) + }) + }) + + var errState = null + , called = false + function next (er) { + if (errState) return + if (er) { + errState = er + return cb(null, []) + } + //console.error('next', installed, obj && typeof obj, name, real) + if (!installed || !obj || !real || called) return + called = true + if (rpSeen[real]) return cb(null, rpSeen[real]) + if (obj === true) { + obj = {dependencies:{}, path:folder} + installed.forEach(function (i) { obj.dependencies[i] = "*" }) + } + if (name && obj.name !== name) obj.invalid = true + obj.realName = name || obj.name + obj.dependencies = obj.dependencies || {} + + // "foo":"http://blah" is always presumed valid + if (reqver + && semver.validRange(reqver) + && !semver.satisfies(obj.version, reqver)) { + obj.invalid = true + } + + if (parent + && !(name in parent.dependencies) + && !(name in (parent.devDependencies || {}))) { + obj.extraneous = true + } + obj.path = obj.path || folder + obj.realPath = real + obj.link = link + if (parent && !obj.link) obj.parent = parent + rpSeen[real] = obj + obj.depth = depth + if (depth >= maxDepth) return cb(null, obj) + asyncMap(installed, function (pkg, cb) { + var rv = obj.dependencies[pkg] + if (!rv && obj.devDependencies) rv = obj.devDependencies[pkg] + readInstalled_( path.resolve(folder, "node_modules/"+pkg) + , obj, pkg, obj.dependencies[pkg], depth + 1, maxDepth + , cb ) + }, function (er, installedData) { + if (er) return cb(er) + installedData.forEach(function (dep) { + obj.dependencies[dep.realName] = dep + }) + + // any strings here are unmet things. however, if it's + // optional, then that's fine, so just delete it. + if (obj.optionalDependencies) { + Object.keys(obj.optionalDependencies).forEach(function (dep) { + if (typeof obj.dependencies[dep] === "string") { + delete obj.dependencies[dep] + } + }) + } + return cb(null, obj) + }) + } +} + +// starting from a root object, call findUnmet on each layer of children +var riSeen = [] +function resolveInheritance (obj) { + if (typeof obj !== "object") return + if (riSeen.indexOf(obj) !== -1) return + riSeen.push(obj) + if (typeof obj.dependencies !== "object") { + obj.dependencies = {} + } + Object.keys(obj.dependencies).forEach(function (dep) { + findUnmet(obj.dependencies[dep]) + }) + Object.keys(obj.dependencies).forEach(function (dep) { + resolveInheritance(obj.dependencies[dep]) + }) +} + +// find unmet deps by walking up the tree object. +// No I/O +var fuSeen = [] +function findUnmet (obj) { + if (fuSeen.indexOf(obj) !== -1) return + fuSeen.push(obj) + //console.error("find unmet", obj.name, obj.parent && obj.parent.name) + var deps = obj.dependencies = obj.dependencies || {} + //console.error(deps) + Object.keys(deps) + .filter(function (d) { return typeof deps[d] === "string" }) + .forEach(function (d) { + //console.error("find unmet", obj.name, d, deps[d]) + var r = obj.parent + , found = null + while (r && !found && typeof deps[d] === "string") { + // if r is a valid choice, then use that. + found = r.dependencies[d] + if (!found && r.realName === d) found = r + + if (!found) { + r = r.link ? null : r.parent + continue + } + if ( typeof deps[d] === "string" + && !semver.satisfies(found.version, deps[d])) { + // the bad thing will happen + log.warn(obj.path + " requires "+d+"@'"+deps[d] + +"' but will load\n" + +found.path+",\nwhich is version "+found.version + ,"unmet dependency") + found.invalid = true + } + deps[d] = found + } + + }) + log.verbose([obj._id], "returning") + return obj +} + +function copy (obj) { + if (!obj || typeof obj !== 'object') return obj + if (Array.isArray(obj)) return obj.map(copy) + + var o = {} + for (var i in obj) o[i] = copy(obj[i]) + return o +} + +if (module === require.main) { + var util = require("util") + console.error("testing") + + var called = 0 + readInstalled(process.cwd(), function (er, map) { + console.error(called ++) + if (er) return console.error(er.stack || er.message) + cleanup(map) + console.error(util.inspect(map, true, 10, true)) + }) + + var seen = [] + function cleanup (map) { + if (seen.indexOf(map) !== -1) return + seen.push(map) + for (var i in map) switch (i) { + case "_id": + case "path": + case "extraneous": case "invalid": + case "dependencies": case "name": + continue + default: delete map[i] + } + var dep = map.dependencies +// delete map.dependencies + if (dep) { +// map.dependencies = dep + for (var i in dep) if (typeof dep[i] === "object") { + cleanup(dep[i]) + } + } + return map + } +} diff --git a/src/static/js/pluginfw/tsort.js b/src/static/js/pluginfw/tsort.js new file mode 100644 index 00000000..6591c51c --- /dev/null +++ b/src/static/js/pluginfw/tsort.js @@ -0,0 +1,112 @@ +/** + * general topological sort + * from https://gist.github.com/1232505 + * @author SHIN Suzuki (shinout310@gmail.com) + * @param Array<Array> edges : list of edges. each edge forms Array<ID,ID> e.g. [12 , 3] + * + * @returns Array : topological sorted list of IDs + **/ + +function tsort(edges) { + var nodes = {}, // hash: stringified id of the node => { id: id, afters: lisf of ids } + sorted = [], // sorted list of IDs ( returned value ) + visited = {}; // hash: id of already visited node => true + + var Node = function(id) { + this.id = id; + this.afters = []; + } + + // 1. build data structures + edges.forEach(function(v) { + var from = v[0], to = v[1]; + if (!nodes[from]) nodes[from] = new Node(from); + if (!nodes[to]) nodes[to] = new Node(to); + nodes[from].afters.push(to); + }); + + // 2. topological sort + Object.keys(nodes).forEach(function visit(idstr, ancestors) { + var node = nodes[idstr], + id = node.id; + + // if already exists, do nothing + if (visited[idstr]) return; + + if (!Array.isArray(ancestors)) ancestors = []; + + ancestors.push(id); + + visited[idstr] = true; + + node.afters.forEach(function(afterID) { + if (ancestors.indexOf(afterID) >= 0) // if already in ancestors, a closed chain exists. + throw new Error('closed chain : ' + afterID + ' is in ' + id); + + visit(afterID.toString(), ancestors.map(function(v) { return v })); // recursive call + }); + + sorted.unshift(id); + }); + + return sorted; +} + +/** + * TEST + **/ +function tsortTest() { + + // example 1: success + var edges = [ + [1, 2], + [1, 3], + [2, 4], + [3, 4] + ]; + + var sorted = tsort(edges); + console.log(sorted); + + // example 2: failure ( A > B > C > A ) + edges = [ + ['A', 'B'], + ['B', 'C'], + ['C', 'A'] + ]; + + try { + sorted = tsort(edges); + } + catch (e) { + console.log(e.message); + } + + // example 3: generate random edges + var max = 100, iteration = 30; + function randomInt(max) { + return Math.floor(Math.random() * max) + 1; + } + + edges = (function() { + var ret = [], i = 0; + while (i++ < iteration) ret.push( [randomInt(max), randomInt(max)] ); + return ret; + })(); + + try { + sorted = tsort(edges); + console.log("succeeded", sorted); + } + catch (e) { + console.log("failed", e.message); + } + +} + + +// for node.js +if (typeof exports == 'object' && exports === this) { + module.exports = tsort; + if (process.argv[1] === __filename) tsortTest(); +} diff --git a/src/static/js/rjquery.js b/src/static/js/rjquery.js new file mode 100644 index 00000000..21af074b --- /dev/null +++ b/src/static/js/rjquery.js @@ -0,0 +1,5 @@ + +// Proviedes a require'able version of jQuery without leaking $ and jQuery; + +require('./jquery'); +exports.jQuery = exports.$ = $.noConflict(true);
\ No newline at end of file diff --git a/static/js/security.js b/src/static/js/security.js index 6f42d051..6f42d051 100644 --- a/static/js/security.js +++ b/src/static/js/security.js diff --git a/static/js/skiplist.js b/src/static/js/skiplist.js index 190bc55b..a02a2ad5 100644 --- a/static/js/skiplist.js +++ b/src/static/js/skiplist.js @@ -20,11 +20,12 @@ * limitations under the License. */ +var Ace2Common = require('./ace2_common'), + _ = require('./underscore'); -var noop = require('./ace2_common').noop; +var noop = Ace2Common.noop; - -function newSkipList() +function SkipList() { var PROFILER = window.PROFILER; if (!PROFILER) @@ -154,6 +155,9 @@ function newSkipList() var widthLoc = point.widthSkips[0] + point.nodes[0].downSkipWidths[0]; var newWidth = _entryWidth(entry); p.mark("loop1"); + + // The new node will have at least level 1 + // With a proability of 0.01^(n-1) the nodes level will be >= n while (newNode.levels == 0 || Math.random() < 0.01) { var lvl = newNode.levels; @@ -284,27 +288,6 @@ function newSkipList() } return dist; } -/*function _debugToString() { - var array = [start]; - while (array[array.length-1] !== end) { - array[array.length] = array[array.length-1].downPtrs[0]; - } - function getIndex(node) { - if (!node) return null; - for(var i=0;i<array.length;i++) { - if (array[i] === node) - return i-1; - } - return false; - } - var processedArray = map(array, function(node) { - var x = {key:node.key, levels: node.levels, downSkips: node.downSkips, - upPtrs: map(node.upPtrs, getIndex), downPtrs: map(node.downPtrs, getIndex), - downSkipWidths: node.downSkipWidths}; - return x; - }); - return map(processedArray, function (x) { return x.toSource(); }).join("\n"); - }*/ function _getNodeByKey(key) { @@ -345,8 +328,9 @@ function newSkipList() /* The skip-list contains "entries", JavaScript objects that each must have a unique "key" property that is a string. -*/ - var self = { + */ + var self = this; + _.extend(this, { length: function() { return numNodes; @@ -482,8 +466,7 @@ that is a string. { return start.levels; } - } - return self; + }); } -exports.newSkipList = newSkipList; +module.exports = SkipList; diff --git a/static/js/timeslider.js b/src/static/js/timeslider.js index 143ef328..438b1695 100644 --- a/static/js/timeslider.js +++ b/src/static/js/timeslider.js @@ -22,13 +22,14 @@ // These jQuery things should create local references, but for now `require()` // assigns to the global `$` and augments it with plugins. -require('/jquery'); -JSON = require('/json2'); -require('/undo-xpopup'); +require('./jquery'); +JSON = require('./json2'); +require('./undo-xpopup'); -var createCookie = require('/pad_utils').createCookie; -var readCookie = require('/pad_utils').readCookie; -var randomString = require('/pad_utils').randomString; +var createCookie = require('./pad_utils').createCookie; +var readCookie = require('./pad_utils').readCookie; +var randomString = require('./pad_utils').randomString; +var _ = require('./underscore'); var socket, token, padId, export_links; @@ -127,17 +128,18 @@ function handleClientVars(message) clientVars = message.data; //load all script that doesn't work without the clientVars - BroadcastSlider = require('/broadcast_slider').loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded); - require('/broadcast_revisions').loadBroadcastRevisionsJS(); - changesetLoader = require('/broadcast').loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider); + BroadcastSlider = require('./broadcast_slider').loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded); + require('./broadcast_revisions').loadBroadcastRevisionsJS(); + changesetLoader = require('./broadcast').loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider); //initialize export ui - require('/pad_impexp').padimpexp.init(); + require('./pad_impexp').padimpexp.init(); //change export urls when the slider moves var export_rev_regex = /(\/\d+)?\/export/ BroadcastSlider.onSlider(function(revno) { + // export_links is a jQuery Array, so .each is allowed. export_links.each(function() { this.setAttribute('href', this.href.replace(export_rev_regex, '/' + revno + '/export')); diff --git a/src/static/js/underscore.js b/src/static/js/underscore.js new file mode 100644 index 00000000..208d4cd8 --- /dev/null +++ b/src/static/js/underscore.js @@ -0,0 +1,999 @@ +// Underscore.js 1.3.1 +// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. +// Underscore is freely distributable under the MIT license. +// Portions of Underscore are inspired or borrowed from Prototype, +// Oliver Steele's Functional, and John Resig's Micro-Templating. +// For all details and documentation: +// http://documentcloud.github.com/underscore + +(function() { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `global` on the server. + var root = this; + + // Save the previous value of the `_` variable. + var previousUnderscore = root._; + + // Establish the object that gets returned to break out of a loop iteration. + var breaker = {}; + + // Save bytes in the minified (but not gzipped) version: + var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; + + // Create quick reference variables for speed access to core prototypes. + var slice = ArrayProto.slice, + unshift = ArrayProto.unshift, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; + + // All **ECMAScript 5** native function implementations that we hope to use + // are declared here. + var + nativeForEach = ArrayProto.forEach, + nativeMap = ArrayProto.map, + nativeReduce = ArrayProto.reduce, + nativeReduceRight = ArrayProto.reduceRight, + nativeFilter = ArrayProto.filter, + nativeEvery = ArrayProto.every, + nativeSome = ArrayProto.some, + nativeIndexOf = ArrayProto.indexOf, + nativeLastIndexOf = ArrayProto.lastIndexOf, + nativeIsArray = Array.isArray, + nativeKeys = Object.keys, + nativeBind = FuncProto.bind; + + // Create a safe reference to the Underscore object for use below. + var _ = function(obj) { return new wrapper(obj); }; + + // Export the Underscore object for **Node.js**, with + // backwards-compatibility for the old `require()` API. If we're in + // the browser, add `_` as a global object via a string identifier, + // for Closure Compiler "advanced" mode. + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = _; + } + exports._ = _; + } else { + root['_'] = _; + } + + // Current version. + _.VERSION = '1.3.1'; + + // Collection Functions + // -------------------- + + // The cornerstone, an `each` implementation, aka `forEach`. + // Handles objects with the built-in `forEach`, arrays, and raw objects. + // Delegates to **ECMAScript 5**'s native `forEach` if available. + var each = _.each = _.forEach = function(obj, iterator, context) { + if (obj == null) return; + if (nativeForEach && obj.forEach === nativeForEach) { + obj.forEach(iterator, context); + } else if (obj.length === +obj.length) { + for (var i = 0, l = obj.length; i < l; i++) { + if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return; + } + } else { + for (var key in obj) { + if (_.has(obj, key)) { + if (iterator.call(context, obj[key], key, obj) === breaker) return; + } + } + } + }; + + // Return the results of applying the iterator to each element. + // Delegates to **ECMAScript 5**'s native `map` if available. + _.map = _.collect = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); + each(obj, function(value, index, list) { + results[results.length] = iterator.call(context, value, index, list); + }); + if (obj.length === +obj.length) results.length = obj.length; + return results; + }; + + // **Reduce** builds up a single result from a list of values, aka `inject`, + // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. + _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + if (nativeReduce && obj.reduce === nativeReduce) { + if (context) iterator = _.bind(iterator, context); + return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); + } + each(obj, function(value, index, list) { + if (!initial) { + memo = value; + initial = true; + } else { + memo = iterator.call(context, memo, value, index, list); + } + }); + if (!initial) throw new TypeError('Reduce of empty array with no initial value'); + return memo; + }; + + // The right-associative version of reduce, also known as `foldr`. + // Delegates to **ECMAScript 5**'s native `reduceRight` if available. + _.reduceRight = _.foldr = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { + if (context) iterator = _.bind(iterator, context); + return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); + } + var reversed = _.toArray(obj).reverse(); + if (context && !initial) iterator = _.bind(iterator, context); + return initial ? _.reduce(reversed, iterator, memo, context) : _.reduce(reversed, iterator); + }; + + // Return the first value which passes a truth test. Aliased as `detect`. + _.find = _.detect = function(obj, iterator, context) { + var result; + any(obj, function(value, index, list) { + if (iterator.call(context, value, index, list)) { + result = value; + return true; + } + }); + return result; + }; + + // Return all the elements that pass a truth test. + // Delegates to **ECMAScript 5**'s native `filter` if available. + // Aliased as `select`. + _.filter = _.select = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); + each(obj, function(value, index, list) { + if (iterator.call(context, value, index, list)) results[results.length] = value; + }); + return results; + }; + + // Return all the elements for which a truth test fails. + _.reject = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + each(obj, function(value, index, list) { + if (!iterator.call(context, value, index, list)) results[results.length] = value; + }); + return results; + }; + + // Determine whether all of the elements match a truth test. + // Delegates to **ECMAScript 5**'s native `every` if available. + // Aliased as `all`. + _.every = _.all = function(obj, iterator, context) { + var result = true; + if (obj == null) return result; + if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); + each(obj, function(value, index, list) { + if (!(result = result && iterator.call(context, value, index, list))) return breaker; + }); + return result; + }; + + // Determine if at least one element in the object matches a truth test. + // Delegates to **ECMAScript 5**'s native `some` if available. + // Aliased as `any`. + var any = _.some = _.any = function(obj, iterator, context) { + iterator || (iterator = _.identity); + var result = false; + if (obj == null) return result; + if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); + each(obj, function(value, index, list) { + if (result || (result = iterator.call(context, value, index, list))) return breaker; + }); + return !!result; + }; + + // Determine if a given value is included in the array or object using `===`. + // Aliased as `contains`. + _.include = _.contains = function(obj, target) { + var found = false; + if (obj == null) return found; + if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; + found = any(obj, function(value) { + return value === target; + }); + return found; + }; + + // Invoke a method (with arguments) on every item in a collection. + _.invoke = function(obj, method) { + var args = slice.call(arguments, 2); + return _.map(obj, function(value) { + return (_.isFunction(method) ? method || value : value[method]).apply(value, args); + }); + }; + + // Convenience version of a common use case of `map`: fetching a property. + _.pluck = function(obj, key) { + return _.map(obj, function(value){ return value[key]; }); + }; + + // Return the maximum element or (element-based computation). + _.max = function(obj, iterator, context) { + if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj); + if (!iterator && _.isEmpty(obj)) return -Infinity; + var result = {computed : -Infinity}; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + computed >= result.computed && (result = {value : value, computed : computed}); + }); + return result.value; + }; + + // Return the minimum element (or element-based computation). + _.min = function(obj, iterator, context) { + if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj); + if (!iterator && _.isEmpty(obj)) return Infinity; + var result = {computed : Infinity}; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + computed < result.computed && (result = {value : value, computed : computed}); + }); + return result.value; + }; + + // Shuffle an array. + _.shuffle = function(obj) { + var shuffled = [], rand; + each(obj, function(value, index, list) { + if (index == 0) { + shuffled[0] = value; + } else { + rand = Math.floor(Math.random() * (index + 1)); + shuffled[index] = shuffled[rand]; + shuffled[rand] = value; + } + }); + return shuffled; + }; + + // Sort the object's values by a criterion produced by an iterator. + _.sortBy = function(obj, iterator, context) { + return _.pluck(_.map(obj, function(value, index, list) { + return { + value : value, + criteria : iterator.call(context, value, index, list) + }; + }).sort(function(left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }), 'value'); + }; + + // Groups the object's values by a criterion. Pass either a string attribute + // to group by, or a function that returns the criterion. + _.groupBy = function(obj, val) { + var result = {}; + var iterator = _.isFunction(val) ? val : function(obj) { return obj[val]; }; + each(obj, function(value, index) { + var key = iterator(value, index); + (result[key] || (result[key] = [])).push(value); + }); + return result; + }; + + // Use a comparator function to figure out at what index an object should + // be inserted so as to maintain order. Uses binary search. + _.sortedIndex = function(array, obj, iterator) { + iterator || (iterator = _.identity); + var low = 0, high = array.length; + while (low < high) { + var mid = (low + high) >> 1; + iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid; + } + return low; + }; + + // Safely convert anything iterable into a real, live array. + _.toArray = function(iterable) { + if (!iterable) return []; + if (iterable.toArray) return iterable.toArray(); + if (_.isArray(iterable)) return slice.call(iterable); + if (_.isArguments(iterable)) return slice.call(iterable); + return _.values(iterable); + }; + + // Return the number of elements in an object. + _.size = function(obj) { + return _.toArray(obj).length; + }; + + // Array Functions + // --------------- + + // Get the first element of an array. Passing **n** will return the first N + // values in the array. Aliased as `head`. The **guard** check allows it to work + // with `_.map`. + _.first = _.head = function(array, n, guard) { + return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; + }; + + // Returns everything but the last entry of the array. Especcialy useful on + // the arguments object. Passing **n** will return all the values in + // the array, excluding the last N. The **guard** check allows it to work with + // `_.map`. + _.initial = function(array, n, guard) { + return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); + }; + + // Get the last element of an array. Passing **n** will return the last N + // values in the array. The **guard** check allows it to work with `_.map`. + _.last = function(array, n, guard) { + if ((n != null) && !guard) { + return slice.call(array, Math.max(array.length - n, 0)); + } else { + return array[array.length - 1]; + } + }; + + // Returns everything but the first entry of the array. Aliased as `tail`. + // Especially useful on the arguments object. Passing an **index** will return + // the rest of the values in the array from that index onward. The **guard** + // check allows it to work with `_.map`. + _.rest = _.tail = function(array, index, guard) { + return slice.call(array, (index == null) || guard ? 1 : index); + }; + + // Trim out all falsy values from an array. + _.compact = function(array) { + return _.filter(array, function(value){ return !!value; }); + }; + + // Return a completely flattened version of an array. + _.flatten = function(array, shallow) { + return _.reduce(array, function(memo, value) { + if (_.isArray(value)) return memo.concat(shallow ? value : _.flatten(value)); + memo[memo.length] = value; + return memo; + }, []); + }; + + // Return a version of the array that does not contain the specified value(s). + _.without = function(array) { + return _.difference(array, slice.call(arguments, 1)); + }; + + // Produce a duplicate-free version of the array. If the array has already + // been sorted, you have the option of using a faster algorithm. + // Aliased as `unique`. + _.uniq = _.unique = function(array, isSorted, iterator) { + var initial = iterator ? _.map(array, iterator) : array; + var result = []; + _.reduce(initial, function(memo, el, i) { + if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) { + memo[memo.length] = el; + result[result.length] = array[i]; + } + return memo; + }, []); + return result; + }; + + // Produce an array that contains the union: each distinct element from all of + // the passed-in arrays. + _.union = function() { + return _.uniq(_.flatten(arguments, true)); + }; + + // Produce an array that contains every item shared between all the + // passed-in arrays. (Aliased as "intersect" for back-compat.) + _.intersection = _.intersect = function(array) { + var rest = slice.call(arguments, 1); + return _.filter(_.uniq(array), function(item) { + return _.every(rest, function(other) { + return _.indexOf(other, item) >= 0; + }); + }); + }; + + // Take the difference between one array and a number of other arrays. + // Only the elements present in just the first array will remain. + _.difference = function(array) { + var rest = _.flatten(slice.call(arguments, 1)); + return _.filter(array, function(value){ return !_.include(rest, value); }); + }; + + // Zip together multiple lists into a single array -- elements that share + // an index go together. + _.zip = function() { + var args = slice.call(arguments); + var length = _.max(_.pluck(args, 'length')); + var results = new Array(length); + for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i); + return results; + }; + + // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), + // we need this function. Return the position of the first occurrence of an + // item in an array, or -1 if the item is not included in the array. + // Delegates to **ECMAScript 5**'s native `indexOf` if available. + // If the array is large and already in sort order, pass `true` + // for **isSorted** to use binary search. + _.indexOf = function(array, item, isSorted) { + if (array == null) return -1; + var i, l; + if (isSorted) { + i = _.sortedIndex(array, item); + return array[i] === item ? i : -1; + } + if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item); + for (i = 0, l = array.length; i < l; i++) if (i in array && array[i] === item) return i; + return -1; + }; + + // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. + _.lastIndexOf = function(array, item) { + if (array == null) return -1; + if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item); + var i = array.length; + while (i--) if (i in array && array[i] === item) return i; + return -1; + }; + + // Generate an integer Array containing an arithmetic progression. A port of + // the native Python `range()` function. See + // [the Python documentation](http://docs.python.org/library/functions.html#range). + _.range = function(start, stop, step) { + if (arguments.length <= 1) { + stop = start || 0; + start = 0; + } + step = arguments[2] || 1; + + var len = Math.max(Math.ceil((stop - start) / step), 0); + var idx = 0; + var range = new Array(len); + + while(idx < len) { + range[idx++] = start; + start += step; + } + + return range; + }; + + // Function (ahem) Functions + // ------------------ + + // Reusable constructor function for prototype setting. + var ctor = function(){}; + + // Create a function bound to a given object (assigning `this`, and arguments, + // optionally). Binding with arguments is also known as `curry`. + // Delegates to **ECMAScript 5**'s native `Function.bind` if available. + // We check for `func.bind` first, to fail fast when `func` is undefined. + _.bind = function bind(func, context) { + var bound, args; + if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); + if (!_.isFunction(func)) throw new TypeError; + args = slice.call(arguments, 2); + return bound = function() { + if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); + ctor.prototype = func.prototype; + var self = new ctor; + var result = func.apply(self, args.concat(slice.call(arguments))); + if (Object(result) === result) return result; + return self; + }; + }; + + // Bind all of an object's methods to that object. Useful for ensuring that + // all callbacks defined on an object belong to it. + _.bindAll = function(obj) { + var funcs = slice.call(arguments, 1); + if (funcs.length == 0) funcs = _.functions(obj); + each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); + return obj; + }; + + // Memoize an expensive function by storing its results. + _.memoize = function(func, hasher) { + var memo = {}; + hasher || (hasher = _.identity); + return function() { + var key = hasher.apply(this, arguments); + return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); + }; + }; + + // Delays a function for the given number of milliseconds, and then calls + // it with the arguments supplied. + _.delay = function(func, wait) { + var args = slice.call(arguments, 2); + return setTimeout(function(){ return func.apply(func, args); }, wait); + }; + + // Defers a function, scheduling it to run after the current call stack has + // cleared. + _.defer = function(func) { + return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); + }; + + // Returns a function, that, when invoked, will only be triggered at most once + // during a given window of time. + _.throttle = function(func, wait) { + var context, args, timeout, throttling, more; + var whenDone = _.debounce(function(){ more = throttling = false; }, wait); + return function() { + context = this; args = arguments; + var later = function() { + timeout = null; + if (more) func.apply(context, args); + whenDone(); + }; + if (!timeout) timeout = setTimeout(later, wait); + if (throttling) { + more = true; + } else { + func.apply(context, args); + } + whenDone(); + throttling = true; + }; + }; + + // Returns a function, that, as long as it continues to be invoked, will not + // be triggered. The function will be called after it stops being called for + // N milliseconds. + _.debounce = function(func, wait) { + var timeout; + return function() { + var context = this, args = arguments; + var later = function() { + timeout = null; + func.apply(context, args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + }; + + // Returns a function that will be executed at most one time, no matter how + // often you call it. Useful for lazy initialization. + _.once = function(func) { + var ran = false, memo; + return function() { + if (ran) return memo; + ran = true; + return memo = func.apply(this, arguments); + }; + }; + + // Returns the first function passed as an argument to the second, + // allowing you to adjust arguments, run code before and after, and + // conditionally execute the original function. + _.wrap = function(func, wrapper) { + return function() { + var args = [func].concat(slice.call(arguments, 0)); + return wrapper.apply(this, args); + }; + }; + + // Returns a function that is the composition of a list of functions, each + // consuming the return value of the function that follows. + _.compose = function() { + var funcs = arguments; + return function() { + var args = arguments; + for (var i = funcs.length - 1; i >= 0; i--) { + args = [funcs[i].apply(this, args)]; + } + return args[0]; + }; + }; + + // Returns a function that will only be executed after being called N times. + _.after = function(times, func) { + if (times <= 0) return func(); + return function() { + if (--times < 1) { return func.apply(this, arguments); } + }; + }; + + // Object Functions + // ---------------- + + // Retrieve the names of an object's properties. + // Delegates to **ECMAScript 5**'s native `Object.keys` + _.keys = nativeKeys || function(obj) { + if (obj !== Object(obj)) throw new TypeError('Invalid object'); + var keys = []; + for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key; + return keys; + }; + + // Retrieve the values of an object's properties. + _.values = function(obj) { + return _.map(obj, _.identity); + }; + + // Return a sorted list of the function names available on the object. + // Aliased as `methods` + _.functions = _.methods = function(obj) { + var names = []; + for (var key in obj) { + if (_.isFunction(obj[key])) names.push(key); + } + return names.sort(); + }; + + // Extend a given object with all the properties in passed-in object(s). + _.extend = function(obj) { + each(slice.call(arguments, 1), function(source) { + for (var prop in source) { + obj[prop] = source[prop]; + } + }); + return obj; + }; + + // Fill in a given object with default properties. + _.defaults = function(obj) { + each(slice.call(arguments, 1), function(source) { + for (var prop in source) { + if (obj[prop] == null) obj[prop] = source[prop]; + } + }); + return obj; + }; + + // Create a (shallow-cloned) duplicate of an object. + _.clone = function(obj) { + if (!_.isObject(obj)) return obj; + return _.isArray(obj) ? obj.slice() : _.extend({}, obj); + }; + + // Invokes interceptor with the obj, and then returns obj. + // The primary purpose of this method is to "tap into" a method chain, in + // order to perform operations on intermediate results within the chain. + _.tap = function(obj, interceptor) { + interceptor(obj); + return obj; + }; + + // Internal recursive comparison function. + function eq(a, b, stack) { + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal. + if (a === b) return a !== 0 || 1 / a == 1 / b; + // A strict comparison is necessary because `null == undefined`. + if (a == null || b == null) return a === b; + // Unwrap any wrapped objects. + if (a._chain) a = a._wrapped; + if (b._chain) b = b._wrapped; + // Invoke a custom `isEqual` method if one is provided. + if (a.isEqual && _.isFunction(a.isEqual)) return a.isEqual(b); + if (b.isEqual && _.isFunction(b.isEqual)) return b.isEqual(a); + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className != toString.call(b)) return false; + switch (className) { + // Strings, numbers, dates, and booleans are compared by value. + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + return a == String(b); + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for + // other numeric values. + return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a == +b; + // RegExps are compared by their source patterns and flags. + case '[object RegExp]': + return a.source == b.source && + a.global == b.global && + a.multiline == b.multiline && + a.ignoreCase == b.ignoreCase; + } + if (typeof a != 'object' || typeof b != 'object') return false; + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + var length = stack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (stack[length] == a) return true; + } + // Add the first object to the stack of traversed objects. + stack.push(a); + var size = 0, result = true; + // Recursively compare objects and arrays. + if (className == '[object Array]') { + // Compare array lengths to determine if a deep comparison is necessary. + size = a.length; + result = size == b.length; + if (result) { + // Deep compare the contents, ignoring non-numeric properties. + while (size--) { + // Ensure commutative equality for sparse arrays. + if (!(result = size in a == size in b && eq(a[size], b[size], stack))) break; + } + } + } else { + // Objects with different constructors are not equivalent. + if ('constructor' in a != 'constructor' in b || a.constructor != b.constructor) return false; + // Deep compare objects. + for (var key in a) { + if (_.has(a, key)) { + // Count the expected number of properties. + size++; + // Deep compare each member. + if (!(result = _.has(b, key) && eq(a[key], b[key], stack))) break; + } + } + // Ensure that both objects contain the same number of properties. + if (result) { + for (key in b) { + if (_.has(b, key) && !(size--)) break; + } + result = !size; + } + } + // Remove the first object from the stack of traversed objects. + stack.pop(); + return result; + } + + // Perform a deep comparison to check if two objects are equal. + _.isEqual = function(a, b) { + return eq(a, b, []); + }; + + // Is a given array, string, or object empty? + // An "empty" object has no enumerable own-properties. + _.isEmpty = function(obj) { + if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; + for (var key in obj) if (_.has(obj, key)) return false; + return true; + }; + + // Is a given value a DOM element? + _.isElement = function(obj) { + return !!(obj && obj.nodeType == 1); + }; + + // Is a given value an array? + // Delegates to ECMA5's native Array.isArray + _.isArray = nativeIsArray || function(obj) { + return toString.call(obj) == '[object Array]'; + }; + + // Is a given variable an object? + _.isObject = function(obj) { + return obj === Object(obj); + }; + + // Is a given variable an arguments object? + _.isArguments = function(obj) { + return toString.call(obj) == '[object Arguments]'; + }; + if (!_.isArguments(arguments)) { + _.isArguments = function(obj) { + return !!(obj && _.has(obj, 'callee')); + }; + } + + // Is a given value a function? + _.isFunction = function(obj) { + return toString.call(obj) == '[object Function]'; + }; + + // Is a given value a string? + _.isString = function(obj) { + return toString.call(obj) == '[object String]'; + }; + + // Is a given value a number? + _.isNumber = function(obj) { + return toString.call(obj) == '[object Number]'; + }; + + // Is the given value `NaN`? + _.isNaN = function(obj) { + // `NaN` is the only value for which `===` is not reflexive. + return obj !== obj; + }; + + // Is a given value a boolean? + _.isBoolean = function(obj) { + return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; + }; + + // Is a given value a date? + _.isDate = function(obj) { + return toString.call(obj) == '[object Date]'; + }; + + // Is the given value a regular expression? + _.isRegExp = function(obj) { + return toString.call(obj) == '[object RegExp]'; + }; + + // Is a given value equal to null? + _.isNull = function(obj) { + return obj === null; + }; + + // Is a given variable undefined? + _.isUndefined = function(obj) { + return obj === void 0; + }; + + // Has own property? + _.has = function(obj, key) { + return hasOwnProperty.call(obj, key); + }; + + // Utility Functions + // ----------------- + + // Run Underscore.js in *noConflict* mode, returning the `_` variable to its + // previous owner. Returns a reference to the Underscore object. + _.noConflict = function() { + root._ = previousUnderscore; + return this; + }; + + // Keep the identity function around for default iterators. + _.identity = function(value) { + return value; + }; + + // Run a function **n** times. + _.times = function (n, iterator, context) { + for (var i = 0; i < n; i++) iterator.call(context, i); + }; + + // Escape a string for HTML interpolation. + _.escape = function(string) { + return (''+string).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g,'/'); + }; + + // Add your own custom functions to the Underscore object, ensuring that + // they're correctly added to the OOP wrapper as well. + _.mixin = function(obj) { + each(_.functions(obj), function(name){ + addToWrapper(name, _[name] = obj[name]); + }); + }; + + // Generate a unique integer id (unique within the entire client session). + // Useful for temporary DOM ids. + var idCounter = 0; + _.uniqueId = function(prefix) { + var id = idCounter++; + return prefix ? prefix + id : id; + }; + + // By default, Underscore uses ERB-style template delimiters, change the + // following template settings to use alternative delimiters. + _.templateSettings = { + evaluate : /<%([\s\S]+?)%>/g, + interpolate : /<%=([\s\S]+?)%>/g, + escape : /<%-([\s\S]+?)%>/g + }; + + // When customizing `templateSettings`, if you don't want to define an + // interpolation, evaluation or escaping regex, we need one that is + // guaranteed not to match. + var noMatch = /.^/; + + // Within an interpolation, evaluation, or escaping, remove HTML escaping + // that had been previously added. + var unescape = function(code) { + return code.replace(/\\\\/g, '\\').replace(/\\'/g, "'"); + }; + + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. + _.template = function(str, data) { + var c = _.templateSettings; + var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' + + 'with(obj||{}){__p.push(\'' + + str.replace(/\\/g, '\\\\') + .replace(/'/g, "\\'") + .replace(c.escape || noMatch, function(match, code) { + return "',_.escape(" + unescape(code) + "),'"; + }) + .replace(c.interpolate || noMatch, function(match, code) { + return "'," + unescape(code) + ",'"; + }) + .replace(c.evaluate || noMatch, function(match, code) { + return "');" + unescape(code).replace(/[\r\n\t]/g, ' ') + ";__p.push('"; + }) + .replace(/\r/g, '\\r') + .replace(/\n/g, '\\n') + .replace(/\t/g, '\\t') + + "');}return __p.join('');"; + var func = new Function('obj', '_', tmpl); + if (data) return func(data, _); + return function(data) { + return func.call(this, data, _); + }; + }; + + // Add a "chain" function, which will delegate to the wrapper. + _.chain = function(obj) { + return _(obj).chain(); + }; + + // The OOP Wrapper + // --------------- + + // If Underscore is called as a function, it returns a wrapped object that + // can be used OO-style. This wrapper holds altered versions of all the + // underscore functions. Wrapped objects may be chained. + var wrapper = function(obj) { this._wrapped = obj; }; + + // Expose `wrapper.prototype` as `_.prototype` + _.prototype = wrapper.prototype; + + // Helper function to continue chaining intermediate results. + var result = function(obj, chain) { + return chain ? _(obj).chain() : obj; + }; + + // A method to easily add functions to the OOP wrapper. + var addToWrapper = function(name, func) { + wrapper.prototype[name] = function() { + var args = slice.call(arguments); + unshift.call(args, this._wrapped); + return result(func.apply(_, args), this._chain); + }; + }; + + // Add all of the Underscore functions to the wrapper object. + _.mixin(_); + + // Add all mutator Array functions to the wrapper. + each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { + var method = ArrayProto[name]; + wrapper.prototype[name] = function() { + var wrapped = this._wrapped; + method.apply(wrapped, arguments); + var length = wrapped.length; + if ((name == 'shift' || name == 'splice') && length === 0) delete wrapped[0]; + return result(wrapped, this._chain); + }; + }); + + // Add all accessor Array functions to the wrapper. + each(['concat', 'join', 'slice'], function(name) { + var method = ArrayProto[name]; + wrapper.prototype[name] = function() { + return result(method.apply(this._wrapped, arguments), this._chain); + }; + }); + + // Start chaining a wrapped Underscore object. + wrapper.prototype.chain = function() { + this._chain = true; + return this; + }; + + // Extracts the result from a wrapped and chained object. + wrapper.prototype.value = function() { + return this._wrapped; + }; + +}).call(this); diff --git a/static/js/undo-xpopup.js b/src/static/js/undo-xpopup.js index e733f3ea..e733f3ea 100644 --- a/static/js/undo-xpopup.js +++ b/src/static/js/undo-xpopup.js diff --git a/static/js/undomodule.js b/src/static/js/undomodule.js index 886cbdf0..4b55311b 100644 --- a/static/js/undomodule.js +++ b/src/static/js/undomodule.js @@ -20,8 +20,8 @@ * limitations under the License. */ -var Changeset = require('/Changeset'); -var extend = require('/ace2_common').extend; +var Changeset = require('./Changeset'); +var _ = require('./underscore'); var undoModule = (function() { @@ -52,7 +52,7 @@ var undoModule = (function() function pushEvent(event) { - var e = extend( + var e = _.extend( {}, event); e.elementType = UNDOABLE_EVENT; stackElements.push(e); diff --git a/static/js/virtual_lines.js b/src/static/js/virtual_lines.js index 2bcf5ed6..2bcf5ed6 100644 --- a/static/js/virtual_lines.js +++ b/src/static/js/virtual_lines.js diff --git a/static/robots.txt b/src/static/robots.txt index 461bb6c9..461bb6c9 100644 --- a/static/robots.txt +++ b/src/static/robots.txt diff --git a/static/tests.html b/src/static/tests.html index bd5bc578..bd5bc578 100644 --- a/static/tests.html +++ b/src/static/tests.html diff --git a/src/templates/admin/plugins.html b/src/templates/admin/plugins.html new file mode 100644 index 00000000..5e5a1b03 --- /dev/null +++ b/src/templates/admin/plugins.html @@ -0,0 +1,168 @@ +<html> + <head> + <title>Plugin manager</title> + <link href="../../static/css/admin.css" rel="stylesheet" type="text/css" /> + <script src="../../static/js/jquery.js"></script> + <script src="../../socket.io/socket.io.js"></script> + <script> + $(document).ready(function () { + var socket = io.connect().of("/pluginfw/installer"); + + var doUpdate = false; + + function updateHandlers() { + $("#progress.dialog .close").unbind('click').click(function () { + $("#progress.dialog").hide(); + }); + + $("#do-search").unbind('click').click(function () { + if ($("#search-query")[0].value != "") + socket.emit("search", $("#search-query")[0].value); + }); + + $(".do-install").unbind('click').click(function (e) { + var row = $(e.target).closest("tr"); + doUpdate = true; + socket.emit("install", row.find(".name").html()); + }); + + $(".do-uninstall").unbind('click').click(function (e) { + var row = $(e.target).closest("tr"); + doUpdate = true; + socket.emit("uninstall", row.find(".name").html()); + }); + } + + updateHandlers(); + + socket.on('progress', function (data) { + $("#progress.dialog .close").hide(); + $("#progress.dialog").show(); + var message = "Unknown status"; + if (data.message) { + message = "<span class='status'>" + data.message.toString() + "</span>"; + } + if (data.error) { + message = "<span class='error'>" + data.error.toString() + "<span>"; + } + $("#progress.dialog .message").html(message); + $("#progress.dialog .history").append("<div>" + message + "</div>"); + + if (data.progress >= 1) { + if (data.error) { + $("#progress.dialog .close").show(); + } else { + if (doUpdate) { + doUpdate = false; + socket.emit("load"); + } + $("#progress.dialog").hide(); + } + } + }); + + socket.on('search-result', function (data) { + $("#search-results *").remove(); + for (plugin_name in data.results) { + var plugin = data.results[plugin_name]; + var row = $("#search-result-template").clone(); + + for (attr in plugin) { + row.find("." + attr).html(plugin[attr]); + } + $("#search-results").append(row); + } + updateHandlers(); + }); + + socket.on('installed-results', function (data) { + $("#installed-plugins *").remove(); + for (plugin_name in data.results) { + var plugin = data.results[plugin_name]; + var row = $("#installed-plugin-template").clone(); + + for (attr in plugin.package) { + row.find("." + attr).html(plugin.package[attr]); + } + $("#installed-plugins").append(row); + } + updateHandlers(); + }); + + socket.emit("load"); + + }); + </script> + </head> + <body> + <% if (errors.length) { %> + <div class="errors"> + <% errors.forEach(function (item) { %> + <div class="error"><%= item.toString() %></div> + <% }) %> + </div> + <% } %> + + + <h1>Installed plugins</h1> + <table> + <thead> + <tr> + <th>Name</th> + <th>Description</th> + <td></td> + </tr> + </thead> + <tbody class="template"> + <tr id="installed-plugin-template"> + <td class="name"></td> + <td class="description"></td> + <td class="actions"> + <input type="button" value="Uninstall" class="do-uninstall"> + </td> + </tr> + </tbody> + <tbody id="installed-plugins"> + </tbody> + </table> + + + + <h1>Search for plugins to install</h1> + <form> + <input type="text" name="search" value="" id="search-query"> + <input type="button" value="Search" id="do-search"> + </form> + <table> + <thead> + <tr> + <th>Name</th> + <th>Description</th> + <td></td> + </tr> + </thead> + <tbody class="template"> + <tr id="search-result-template"> + <td class="name"></td> + <td class="description"></td> + <td class="actions"> + <input type="button" value="Install" class="do-install"> + </td> + </tr> + </tbody> + <tbody id="search-results"> + </tbody> + </table> + + + <div id="progress" class="dialog"> + <h1 class="title"> + Please wait: <span class="message"></span> + <input type="button" class="close" value="Close"> + </h1> + + <div class="history"></div> + </div> + + </body> +</html> diff --git a/static/index.html b/src/templates/index.html index 58f68801..ecfa0d9e 100644 --- a/static/index.html +++ b/src/templates/index.html @@ -130,6 +130,7 @@ <script src="static/custom/index.js"></script> <script> + document.domain = document.domain; function go2Name() { var padname = document.getElementById("padname").value; diff --git a/src/templates/pad.html b/src/templates/pad.html new file mode 100644 index 00000000..533bb2ba --- /dev/null +++ b/src/templates/pad.html @@ -0,0 +1,308 @@ +<% + var settings = require("ep_etherpad-lite/node/utils/Settings"); +%> +<!doctype html> +<html> + + <title>Etherpad Lite</title> + + <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"> + + <% e.begin_block("styles"); %> + <link href="../static/css/pad.css" rel="stylesheet"> + <link href="../static/custom/pad.css" rel="stylesheet"> + <style title="dynamicsyntax"></style> + <% e.end_block(); %> + + <!-- head and body had been removed intentionally --> + + <div id="editbar" class="toolbar"> + <ul class="menu_left"> + <% e.begin_block("editbarMenuLeft"); %> + <li id="bold" onClick="window.pad&&pad.editbarClick('bold');return false"> + <a class="buttonicon buttonicon-bold" title="Bold (ctrl-B)"></a> + </li> + <li id="italic" onClick="window.pad&&pad.editbarClick('italic'); return false;"> + <a class="buttonicon buttonicon-italic" title="Italics (ctrl-I)"></a> + </li> + <li id="underline" onClick="window.pad&&pad.editbarClick('underline');return false;" > + <a class="buttonicon buttonicon-underline" title="Underline (ctrl-U)"></a> + </li> + <li id="strikethrough" onClick="window.pad&&pad.editbarClick('strikethrough');return false;"> + <a class="buttonicon buttonicon-strikethrough" title="Strikethrough"></a> + </li> + <li class="separator"></li> + <li id="oderedlist" onClick="window.pad&&pad.editbarClick('insertorderedlist');return false;"> + <a class="buttonicon buttonicon-insertorderedlist" title="Toggle Ordered List"></a> + </li> + <li id="unoderedlist" onClick="window.pad&&pad.editbarClick('insertunorderedlist');return false;"> + <a class="buttonicon buttonicon-insertunorderedlist" title="Toggle Bullet List"></a> + </li> + <li id="indent" onClick="window.pad&&pad.editbarClick('indent');return false;"> + <a class="buttonicon buttonicon-indent" title="Indent"></a> + </li> + <li id="outdent" onClick="window.pad&&pad.editbarClick('outdent');return false;"> + <a class="buttonicon buttonicon-outdent" title="Unindent"></a> + </li> + <li class="separator"></li> + <li id="undo" onClick="window.pad&&pad.editbarClick('undo');return false;"> + <a class="buttonicon buttonicon-undo" title="Undo (ctrl-Z)"></a> + </li> + <li id="redo" onClick="window.pad&&pad.editbarClick('redo');return false;"> + <a class="buttonicon buttonicon-redo" title="Redo (ctrl-Y)"></a> + </li> + <li class="separator"></li> + <li id="clearAuthorship" onClick="window.pad&&pad.editbarClick('clearauthorship');return false;"> + <a class="buttonicon buttonicon-clearauthorship" title="Clear Authorship Colors"></a> + </li> + <% e.end_block(); %> + </ul> + <ul class="menu_right"> + <% e.begin_block("editbarMenuRight"); %> + <li onClick="window.pad&&pad.editbarClick('savedRevision');return false;"> + <a id="settingslink" title="Mark this revision as a saved revision"> + <div class="buttonicon buttonicon-savedRevision"></div> + </a> + </li> + <li id="settingslink" onClick="window.pad&&pad.editbarClick('settings');return false;"> + <a class="buttonicon buttonicon-settings" id="settingslink" title="Settings of this pad"></a> + </li> + <li id="importexportlink" onClick="window.pad&&pad.editbarClick('import_export');return false;"> + <a class="buttonicon buttonicon-import_export" id="exportlink" title="Import/Export from/to different document formats"></a> + </li> + <li id="embedlink" onClick="window.pad&&pad.editbarClick('embed');return false;" > + <a class="buttonicon buttonicon-embed" id="embedlink" title="Share and Embed this pad"></a> + </li> + <li class="separator"></li> + <li id="timesliderlink" onClick="document.location = document.location.pathname+ '/timeslider'"> + <a class="buttonicon buttonicon-history" title="Show the history of this pad"></a> + </li> + <li id="usericon" onClick="window.pad&&pad.editbarClick('showusers');return false;" title="Show connected users"> + <span class="buttonicon buttonicon-showusers" id="usericonback"></span> + <span id="online_count">1</span> + </li> + <% e.end_block(); %> + </ul> + </div> + + <div id="users"> + <div id="connectionstatus"></div> + <div id="myuser"> + <div id="mycolorpicker"> + <div id="colorpicker"></div> + <button id="mycolorpickersave">Save</button> + <button id="mycolorpickercancel">Cancel</button> + <span id="mycolorpickerpreview" class="myswatchboxhoverable"></span> + </div> + <div id="myswatchbox"><div id="myswatch"></div></div> + <div id="myusernameform"><input type="text" id="myusernameedit" disabled="disabled"></div> + <div id="mystatusform"><input type="text" id="mystatusedit" disabled="disabled"></div> + </div> + <div id="otherusers"> + <div id="guestprompts"></div> + <table id="otheruserstable" cellspacing="0" cellpadding="0" border="0"> + <tr><td></td></tr> + </table> + <div id="nootherusers"></div> + </div> + <div id="userlistbuttonarea"></div> + </div> + + <div id="editorcontainerbox"> + <div id="editorcontainer"></div> + <div id="editorloadingbox">Loading...</div> + </div> + + <div id="settingsmenu" class="popup"> + <h1>Pad settings</h1> + <div class="column"> + <h2>My view</h2> + <p> + <input type="checkbox" id="options-stickychat" onClick="chat.stickToScreen();"> + <label for="options-stickychat">Chat always on screen</label> + </p> + <p> + <input type="checkbox" id="options-colorscheck"> + <label for="options-colorscheck">Authorship colors</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> + </p> + </div> + <div class="column"> + <h2>Global view</h2> + <p>Currently nothing.</p> + <p class="note">These options affect everyone viewing this pad.</p> + </div> + </div> + + <div id="importexport" class="popup"> + <div class="column"> + <h2>Import from text file, HTML, PDF, Word, ODT or RTF</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="importformdiv" id="importformsubmitdiv"> + <input type="hidden" name="padId" value="blpmaXT35R"> + <span class="nowrap"> + <input type="submit" name="submit" value="Import Now" disabled="disabled" id="importsubmitinput"> + <img alt="" id="importstatusball" src="../static/img/loading.gif" align="top"> + <img alt="" id="importarrow" src="../static/img/leftarrow.png" align="top"> + </span> + </div> + </form> + </div> + <div class="column"> + <h2>Export current pad as</h2> + <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="exportwordlea" target="_blank" onClick="padimpexp.export2Wordle();return false;" class="exportlink"><div class="exporttype" id="exportwordle">Wordle</div></a> + </div> + </div> + + <div id="embed" class="popup"> + <div id="embedreadonly" class="right"> + <input type="checkbox" id="readonlyinput" onClick="padeditbar.setEmbedLinks();"> + <label for="readonlyinput">Read only</label> + </div> + <h1>Share this pad</h1> + <div id="linkcode"> + <h2>Link</h2> + <input id="linkinput" type="text" value=""> + </div> + <br> + <div id="embedcode"> + <h2>Embed URL</h2> + <input id="embedinput" type="text" value=""> + </div> + <br> + <div id="qrcode"> + <h2>QR code</h2> + <div id="qr_center"><img id="embedreadonlyqr"></div> + </div> + </div> + + <div id="chatthrob"></div> + + <div id="chaticon" title="Open the chat for this pad" onclick="chat.show();return false;"> + <span id="chatlabel">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;">- </a></div> + <div id="chattext" class="authorColors"></div> + <div id="chatinputbox"> + <form> + <input id="chatinput" type="text" maxlength="140"> + </form> + </div> + </div> + + <div id="focusprotector"> </div> + + <div id="modaloverlay"> + <div id="modaloverlay-inner"></div> + </div> + + <div id="mainmodals"> + <% e.begin_block("modals"); %> + <div id="connectionbox" class="modaldialog"> + <div id="connectionboxinner" class="modaldialog-inner"> + <div class="connecting">Connecting...</div> + <div class="reconnecting">Reestablishing connection...</div> + <div class="disconnected"> + <h2 class="h2_disconnect">Disconnected.</h2> + <h2 class="h2_userdup">Opened in another window.</h2> + <h2 class="h2_unauth">No Authorization.</h2> + <div id="disconnected_looping"> + <p><b>We're having trouble talking to the EtherPad lite synchronization server.</b> You may be connecting through an incompatible firewall or proxy server.</p> + </div> + <div id="disconnected_initsocketfail"> + <p><b>We were unable to connect to the EtherPad lite synchronization server.</b> This may be due to an incompatibility with your web browser or internet connection.</p> + </div> + <div id="disconnected_userdup"> + <p><b>You seem to have opened this pad in another browser window.</b> If you'd like to use this window instead, you can reconnect.</p> + </div> + <div id="disconnected_unknown"> + <p><b>Lost connection with the EtherPad lite synchronization server.</b> This may be due to a loss of network connectivity.</p> + </div> + <div id="disconnected_slowcommit"> + <p><b>Server not responding.</b> This may be due to network connectivity issues or high load on the server.</p> + </div> + <div id="disconnected_unauth"> + <p>Your browser's credentials or permissions have changed while viewing this pad. Try reconnecting.</p> + </div> + <div id="disconnected_deleted"> + <p>This pad was deleted.</p> + </div> + <div id="reconnect_advise"> + <p>If this continues to happen, please let us know</p> + </div> + <div id="reconnect_form"> + <button id="forcereconnect">Reconnect Now</button> + </div> + </div> + </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> + </div> + <% e.end_block(); %> + </div> + + <% e.begin_block("scripts"); %> + <script type="text/javascript" src="../static/js/require-kernel.js"></script> + <script type="text/javascript" src="../static/js/jquery.js"></script> + <script type="text/javascript" src="../socket.io/socket.io.js"></script> + <% if (settings.minify) { %> + <script type="text/javascript" src="../javascripts/lib/ep_etherpad-lite/static/js/pad.js?callback=require.define"></script> + <% } %> + <script type="text/javascript"> + document.domain = document.domain; + var clientVars = {}; + (function () { + <% if (settings.minify) { %> + require.setRootURI("../javascripts/src"); + require.setLibraryURI("../javascripts/lib"); + require.setGlobalKeyPath("require"); + <% } else { %> + require.setRootURI("../static/js"); + require.setLibraryURI("../static/plugins"); + <% } %> + + var plugins = require('ep_etherpad-lite/static/js/pluginfw/plugins'); + plugins.update(function () { + require('ep_etherpad-lite/static/js/pad').init(); + }); + + /* TODO: These globals shouldn't exist. */ + pad = require('ep_etherpad-lite/static/js/pad').pad; + chat = require('ep_etherpad-lite/static/js/chat').chat; + padeditbar = require('ep_etherpad-lite/static/js/pad_editbar').padeditbar; + padimpexp = require('ep_etherpad-lite/static/js/pad_impexp').padimpexp; + }()); + </script> + <% e.end_block(); %> +</html> diff --git a/src/templates/timeslider.html b/src/templates/timeslider.html new file mode 100644 index 00000000..ca4bc6e9 --- /dev/null +++ b/src/templates/timeslider.html @@ -0,0 +1,151 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta name="robots" content="noindex, nofollow"> + + <title>Etherpad Lite Timeslider</title> + <link rel="stylesheet" href="../../static/css/pad.css"> + <link rel="stylesheet" href="../../static/css/timeslider.css"> + <link rel="stylesheet" href="../../static/custom/timeslider.css"> + <style type="text/css" title="dynamicsyntax"></style> +</head> + +<body id="padbody" class="timeslider limwidth"> + <div id="padpage"> + <div id="padtop"> + <div id="alertbar"> + <div id="servermsg"> + <h3>Server Notice<span id="servermsgdate"><!-- --></span>:</h3><a id= + "hidetopmsg" href="javascript:%20void%20pad.hideServerMessage()" name= + "hidetopmsg">hide</a> + <p id="servermsgtext"><!-- --></p> + </div> + </div> + + <div id="navigation"></div> + + <div id="docbar" class="menu docbar"> + <table border="0" cellpadding="0" cellspacing="0" width="100%" id="docbartable" + class="docbartable"> + <tr> + <td><img src="../../static/img/roundcorner_left.gif" /></td> + + <td id="docbarpadtitle" class="docbarpadtitle" title= + "Public Pad: Public Pad"><span>Public Pad</span></td> + + <td width="100%"> </td> + + <td><img src="../../static/img/roundcorner_right.gif" /></td> + </tr> + </table> + </div><!-- /docbar --> + </div> + + <div id="timeslider-top"> + <div id="timeslider-wrapper"> + <div id="timeslider" unselectable="on" style="display: none"> + <div id="timeslider-left"></div> + <div id="timeslider-right"></div> + <div id="timer"></div> + <div id="timeslider-slider"> + <div id="ui-slider-handle"></div> + <div id="ui-slider-bar"></div> + </div> + <div id="playpause_button"> + <div id="playpause_button_icon" class=""></div> + </div> + <div id="steppers"> + <div class="stepper" id="leftstep"></div> + <div class="stepper" id="rightstep"></div> + </div> + </div> + </div> + + <div class="timeslider-bar"> + <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"> + <div class="buttonicon buttonicon-import_export"></div> + </a> + </li> + </ul> + <a id="returnbutton">Return to pad</a> + </div> + + <div> + <h1> + <span id="revision_label"></span> + <span id="revision_date"></span> + </h1> + <p>Authors: + <span id="authorsList"> + <span>No Authors</span> + </span> </p> + </div> + </div> + </div> + + <div id="padmain"> + <div id="padeditor"> + <div id="editorcontainerbox"> + <div id="padcontent"> + </div> + </div> + </div><!-- /padeditor --> + </div><!-- /padmain --> + </div><!-- /padpage --> + + <div id="modaloverlay"> + <div id="modaloverlay-inner"> + <!-- --> + </div> + </div> + + <div id="mainmodals"></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> + <a id="exportwordlea" target="_blank" onClick="padimpexp.export2Wordle();return false;" class="exportlink"><div class="exporttype" id="exportwordle">Wordle</div></a> + <form id="wordlepost" name="wall" action="http://wordle.net/advanced" method="POST" style="margin-left:0px;"> + <div id="hidetext" style=""><textarea id="text" name="text" id="text" style="display:none;">Coming soon!</textarea></div> + </form> +</div> +</div> + +<script type="text/javascript" src="../../../static/js/require-kernel.js"></script> +<script type="text/javascript" src="../../../static/js/jquery.js"></script> +<script type="text/javascript" src="../../../socket.io/socket.io.js"></script> +<script type="text/javascript" src="../../../javascripts/lib/ep_etherpad-lite/static/js/timeslider.js?callback=require.define"></script> +<script type="text/javascript" src="../../../static/custom/timeslider.js"></script> +<script type="text/javascript" > + document.domain = document.domain; + var clientVars = {}; + (function () { + require.setRootURI("../../../javascripts/src"); + require.setLibraryURI("../../../javascripts/lib"); + require.setGlobalKeyPath("require"); + + var plugins = require('ep_etherpad-lite/static/js/pluginfw/plugins'); + plugins.update(function () { + require('ep_etherpad-lite/static/js/timeslider').init(); + + /* TODO: These globals shouldn't exist. */ + padeditbar = require('ep_etherpad-lite/static/js/pad_editbar').padeditbar; + padimpexp = require('ep_etherpad-lite/static/js/pad_impexp').padimpexp; + }); + })(); +</script> +</body> +</html> + diff --git a/static/custom/.gitignore b/static/custom/.gitignore deleted file mode 100644 index 8cdbf183..00000000 --- a/static/custom/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -* -!js.template -!css.template
\ No newline at end of file diff --git a/static/img/etherpad_lite_icons.png b/static/img/etherpad_lite_icons.png Binary files differdeleted file mode 100644 index cadf5ed2..00000000 --- a/static/img/etherpad_lite_icons.png +++ /dev/null diff --git a/static/js/AttributePoolFactory.js b/static/js/AttributePoolFactory.js deleted file mode 100644 index 00b58dbb..00000000 --- a/static/js/AttributePoolFactory.js +++ /dev/null @@ -1,90 +0,0 @@ -/** - * This code represents the Attribute Pool Object of the original Etherpad. - * 90% of the code is still like in the original Etherpad - * Look at https://github.com/ether/pad/blob/master/infrastructure/ace/www/easysync2.js - * You can find a explanation what a attribute pool is here: - * https://github.com/Pita/etherpad-lite/blob/master/doc/easysync/easysync-notes.txt - */ - -/* - * Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -exports.createAttributePool = function () { - var p = {}; - p.numToAttrib = {}; // e.g. {0: ['foo','bar']} - p.attribToNum = {}; // e.g. {'foo,bar': 0} - p.nextNum = 0; - - p.putAttrib = function (attrib, dontAddIfAbsent) { - var str = String(attrib); - if (str in p.attribToNum) { - return p.attribToNum[str]; - } - if (dontAddIfAbsent) { - return -1; - } - var num = p.nextNum++; - p.attribToNum[str] = num; - p.numToAttrib[num] = [String(attrib[0] || ''), String(attrib[1] || '')]; - return num; - }; - - p.getAttrib = function (num) { - var pair = p.numToAttrib[num]; - if (!pair) { - return pair; - } - return [pair[0], pair[1]]; // return a mutable copy - }; - - p.getAttribKey = function (num) { - var pair = p.numToAttrib[num]; - if (!pair) return ''; - return pair[0]; - }; - - p.getAttribValue = function (num) { - var pair = p.numToAttrib[num]; - if (!pair) return ''; - return pair[1]; - }; - - p.eachAttrib = function (func) { - for (var n in p.numToAttrib) { - var pair = p.numToAttrib[n]; - func(pair[0], pair[1]); - } - }; - - p.toJsonable = function () { - return { - numToAttrib: p.numToAttrib, - nextNum: p.nextNum - }; - }; - - p.fromJsonable = function (obj) { - p.numToAttrib = obj.numToAttrib; - p.nextNum = obj.nextNum; - p.attribToNum = {}; - for (var n in p.numToAttrib) { - p.attribToNum[String(p.numToAttrib[n])] = Number(n); - } - return p; - }; - - return p; -} diff --git a/static/js/pad_savedrevs.js b/static/js/pad_savedrevs.js deleted file mode 100644 index 6d37dfa8..00000000 --- a/static/js/pad_savedrevs.js +++ /dev/null @@ -1,526 +0,0 @@ -/** - * This code is mostly from the old Etherpad. Please help us to comment this code. - * This helps other people to understand this code better and helps them to improve it. - * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED - */ - -/** - * Copyright 2009 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var padutils = require('/pad_utils').padutils; -var paddocbar = require('/pad_docbar').paddocbar; - -var padsavedrevs = (function() -{ - - function reversedCopy(L) - { - var L2 = L.slice(); - L2.reverse(); - return L2; - } - - function makeRevisionBox(revisionInfo, rnum) - { - var box = $('<div class="srouterbox">' + '<div class="srinnerbox">' + '<a href="javascript:void(0)" class="srname"><!-- --></a>' + '<div class="sractions"><a class="srview" href="javascript:void(0)" target="_blank">view</a> | <a class="srrestore" href="javascript:void(0)">restore</a></div>' + '<div class="srtime"><!-- --></div>' + '<div class="srauthor"><!-- --></div>' + '<img class="srtwirly" src="static/img/misc/status-ball.gif">' + '</div></div>'); - setBoxLabel(box, revisionInfo.label); - setBoxTimestamp(box, revisionInfo.timestamp); - box.find(".srauthor").html("by " + padutils.escapeHtml(revisionInfo.savedBy)); - var viewLink = '/ep/pad/view/' + pad.getPadId() + '/' + revisionInfo.id; - box.find(".srview").attr('href', viewLink); - var restoreLink = 'javascript:void(require('+JSON.stringify(module.id)+').padsavedrevs.restoreRevision(' + JSON.stringify(rnum) + ');'; - box.find(".srrestore").attr('href', restoreLink); - box.find(".srname").click(function(evt) - { - editRevisionLabel(rnum, box); - }); - return box; - } - - function setBoxLabel(box, label) - { - box.find(".srname").html(padutils.escapeHtml(label)).attr('title', label); - } - - function setBoxTimestamp(box, timestamp) - { - box.find(".srtime").html(padutils.escapeHtml( - padutils.timediff(new Date(timestamp)))); - } - - function getNthBox(n) - { - return $("#savedrevisions .srouterbox").eq(n); - } - - function editRevisionLabel(rnum, box) - { - var input = $('<input type="text" class="srnameedit"/>'); - box.find(".srnameedit").remove(); // just in case - var label = box.find(".srname"); - input.width(label.width()); - input.height(label.height()); - input.css('top', label.position().top); - input.css('left', label.position().left); - label.after(input); - label.css('opacity', 0); - - function endEdit() - { - input.remove(); - label.css('opacity', 1); - } - var rev = currentRevisionList[rnum]; - var oldLabel = rev.label; - input.blur(function() - { - var newLabel = input.val(); - if (newLabel && newLabel != oldLabel) - { - relabelRevision(rnum, newLabel); - } - endEdit(); - }); - input.val(rev.label).focus().select(); - padutils.bindEnterAndEscape(input, function onEnter() - { - input.blur(); - }, function onEscape() - { - input.val('').blur(); - }); - } - - function relabelRevision(rnum, newLabel) - { - var rev = currentRevisionList[rnum]; - $.ajax( - { - type: 'post', - url: '/ep/pad/saverevisionlabel', - data: { - userId: pad.getUserId(), - padId: pad.getPadId(), - revId: rev.id, - newLabel: newLabel - }, - success: success, - error: error - }); - - function success(text) - { - var newRevisionList = JSON.parse(text); - self.newRevisionList(newRevisionList); - pad.sendClientMessage( - { - type: 'revisionLabel', - revisionList: reversedCopy(currentRevisionList), - savedBy: pad.getUserName(), - newLabel: newLabel - }); - } - - function error(e) - { - alert("Oops! There was an error saving that revision label. Please try again later."); - } - } - - var currentRevisionList = []; - - function setRevisionList(newRevisionList, noAnimation) - { - // deals with changed labels and new added revisions - for (var i = 0; i < currentRevisionList.length; i++) - { - var a = currentRevisionList[i]; - var b = newRevisionList[i]; - if (b.label != a.label) - { - setBoxLabel(getNthBox(i), b.label); - } - } - for (var j = currentRevisionList.length; j < newRevisionList.length; j++) - { - var newBox = makeRevisionBox(newRevisionList[j], j); - $("#savedrevs-scrollinner").append(newBox); - newBox.css('left', j * REVISION_BOX_WIDTH); - } - var newOnes = (newRevisionList.length > currentRevisionList.length); - currentRevisionList = newRevisionList; - if (newOnes) - { - setDesiredScroll(getMaxScroll()); - if (noAnimation) - { - setScroll(desiredScroll); - } - - if (!noAnimation) - { - var nameOfLast = currentRevisionList[currentRevisionList.length - 1].label; - displaySavedTip(nameOfLast); - } - } - } - - function refreshRevisionList() - { - for (var i = 0; i < currentRevisionList.length; i++) - { - var r = currentRevisionList[i]; - var box = getNthBox(i); - setBoxTimestamp(box, r.timestamp); - } - } - - var savedTipAnimator = padutils.makeShowHideAnimator(function(state) - { - if (state == -1) - { - $("#revision-notifier").css('opacity', 0).css('display', 'block'); - } - else if (state == 0) - { - $("#revision-notifier").css('opacity', 1); - } - else if (state == 1) - { - $("#revision-notifier").css('opacity', 0).css('display', 'none'); - } - else if (state < 0) - { - $("#revision-notifier").css('opacity', 1); - } - else if (state > 0) - { - $("#revision-notifier").css('opacity', 1 - state); - } - }, false, 25, 300); - - function displaySavedTip(text) - { - $("#revision-notifier .name").html(padutils.escapeHtml(text)); - savedTipAnimator.show(); - padutils.cancelActions("hide-revision-notifier"); - var hideLater = padutils.getCancellableAction("hide-revision-notifier", function() - { - savedTipAnimator.hide(); - }); - window.setTimeout(hideLater, 3000); - } - - var REVISION_BOX_WIDTH = 120; - var curScroll = 0; // distance between left of revisions and right of view - var desiredScroll = 0; - - function getScrollWidth() - { - return REVISION_BOX_WIDTH * currentRevisionList.length; - } - - function getViewportWidth() - { - return $("#savedrevs-scrollouter").width(); - } - - function getMinScroll() - { - return Math.min(getViewportWidth(), getScrollWidth()); - } - - function getMaxScroll() - { - return getScrollWidth(); - } - - function setScroll(newScroll) - { - curScroll = newScroll; - $("#savedrevs-scrollinner").css('right', newScroll); - updateScrollArrows(); - } - - function setDesiredScroll(newDesiredScroll, dontUpdate) - { - desiredScroll = Math.min(getMaxScroll(), Math.max(getMinScroll(), newDesiredScroll)); - if (!dontUpdate) - { - updateScroll(); - } - } - - function updateScroll() - { - updateScrollArrows(); - scrollAnimator.scheduleAnimation(); - } - - function updateScrollArrows() - { - $("#savedrevs-scrollleft").toggleClass("disabledscrollleft", desiredScroll <= getMinScroll()); - $("#savedrevs-scrollright").toggleClass("disabledscrollright", desiredScroll >= getMaxScroll()); - } - var scrollAnimator = padutils.makeAnimationScheduler(function() - { - setDesiredScroll(desiredScroll, true); // re-clamp - if (Math.abs(desiredScroll - curScroll) < 1) - { - setScroll(desiredScroll); - return false; - } - else - { - setScroll(curScroll + (desiredScroll - curScroll) * 0.5); - return true; - } - }, 50, 2); - - var isSaving = false; - - function setIsSaving(v) - { - isSaving = v; - rerenderButton(); - } - - function haveReachedRevLimit() - { - var mv = pad.getPrivilege('maxRevisions'); - return (!(mv < 0 || mv > currentRevisionList.length)); - } - - function rerenderButton() - { - if (isSaving || (!pad.isFullyConnected()) || haveReachedRevLimit()) - { - $("#savedrevs-savenow").css('opacity', 0.75); - } - else - { - $("#savedrevs-savenow").css('opacity', 1); - } - } - - var scrollRepeatTimer = null; - var scrollStartTime = 0; - - function setScrollRepeatTimer(dir) - { - clearScrollRepeatTimer(); - scrollStartTime = +new Date; - scrollRepeatTimer = window.setTimeout(function f() - { - if (!scrollRepeatTimer) - { - return; - } - self.scroll(dir); - var scrollTime = (+new Date) - scrollStartTime; - var delay = (scrollTime > 2000 ? 50 : 300); - scrollRepeatTimer = window.setTimeout(f, delay); - }, 300); - $(document).bind('mouseup', clearScrollRepeatTimer); - } - - function clearScrollRepeatTimer() - { - if (scrollRepeatTimer) - { - window.clearTimeout(scrollRepeatTimer); - scrollRepeatTimer = null; - } - $(document).unbind('mouseup', clearScrollRepeatTimer); - } - - var pad = undefined; - var self = { - init: function(initialRevisions, _pad) - { - pad = _pad; - self.newRevisionList(initialRevisions, true); - - $("#savedrevs-savenow").click(function() - { - self.saveNow(); - }); - $("#savedrevs-scrollleft").mousedown(function() - { - self.scroll('left'); - setScrollRepeatTimer('left'); - }); - $("#savedrevs-scrollright").mousedown(function() - { - self.scroll('right'); - setScrollRepeatTimer('right'); - }); - $("#savedrevs-close").click(function() - { - paddocbar.setShownPanel(null); - }); - - // update "saved n minutes ago" times - window.setInterval(function() - { - refreshRevisionList(); - }, 60 * 1000); - }, - restoreRevision: function(rnum) - { - var rev = currentRevisionList[rnum]; - var warning = ("Restoring this revision will overwrite the current" + " text of the pad. " + "Are you sure you want to continue?"); - var hidePanel = paddocbar.hideLaterIfNoOtherInteraction(); - var box = getNthBox(rnum); - if (confirm(warning)) - { - box.find(".srtwirly").show(); - $.ajax( - { - type: 'get', - url: '/ep/pad/getrevisionatext', - data: { - padId: pad.getPadId(), - revId: rev.id - }, - success: success, - error: error - }); - } - - function success(resultJson) - { - untwirl(); - var result = JSON.parse(resultJson); - padeditor.restoreRevisionText(result); - window.setTimeout(function() - { - hidePanel(); - }, 0); - } - - function error(e) - { - untwirl(); - alert("Oops! There was an error retreiving the text (revNum= " + rev.revNum + "; padId=" + pad.getPadId()); - } - - function untwirl() - { - box.find(".srtwirly").hide(); - } - }, - showReachedLimit: function() - { - alert("Sorry, you do not have privileges to save more than " + pad.getPrivilege('maxRevisions') + " revisions."); - }, - newRevisionList: function(lst, noAnimation) - { - // server gives us list with newest first; - // we want chronological order - var L = reversedCopy(lst); - setRevisionList(L, noAnimation); - rerenderButton(); - }, - saveNow: function() - { - if (isSaving) - { - return; - } - if (!pad.isFullyConnected()) - { - return; - } - if (haveReachedRevLimit()) - { - self.showReachedLimit(); - return; - } - setIsSaving(true); - var savedBy = pad.getUserName() || "unnamed"; - pad.callWhenNotCommitting(submitSave); - - function submitSave() - { - $.ajax( - { - type: 'post', - url: '/ep/pad/saverevision', - data: { - padId: pad.getPadId(), - savedBy: savedBy, - savedById: pad.getUserId(), - revNum: pad.getCollabRevisionNumber() - }, - success: success, - error: error - }); - } - - function success(text) - { - setIsSaving(false); - var newRevisionList = JSON.parse(text); - self.newRevisionList(newRevisionList); - pad.sendClientMessage( - { - type: 'newRevisionList', - revisionList: newRevisionList, - savedBy: savedBy - }); - } - - function error(e) - { - setIsSaving(false); - alert("Oops! The server failed to save the revision. Please try again later."); - } - }, - handleResizePage: function() - { - updateScrollArrows(); - }, - handleIsFullyConnected: function(isConnected) - { - rerenderButton(); - }, - scroll: function(dir) - { - var minScroll = getMinScroll(); - var maxScroll = getMaxScroll(); - if (dir == 'left') - { - if (desiredScroll > minScroll) - { - var n = Math.floor((desiredScroll - 1 - minScroll) / REVISION_BOX_WIDTH); - setDesiredScroll(Math.max(0, n) * REVISION_BOX_WIDTH + minScroll); - } - } - else if (dir == 'right') - { - if (desiredScroll < maxScroll) - { - var n = Math.floor((maxScroll - desiredScroll - 1) / REVISION_BOX_WIDTH); - setDesiredScroll(maxScroll - Math.max(0, n) * REVISION_BOX_WIDTH); - } - } - } - }; - return self; -}()); - -exports.padsavedrevs = padsavedrevs; diff --git a/static/js/plugins.js b/static/js/plugins.js deleted file mode 100644 index 3e226eba..00000000 --- a/static/js/plugins.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * This code is mostly from the old Etherpad. Please help us to comment this code. - * This helps other people to understand this code better and helps them to improve it. - * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED - */ - -var plugins = { - callHook: function(hookName, args) - { - var global = (function () {return this}()); - var hook = ((global.clientVars || {}).hooks || {})[hookName]; - if (hook === undefined) return []; - var res = []; - for (var i = 0, N = hook.length; i < N; i++) - { - var plugin = hook[i]; - var pluginRes = eval(plugin.plugin)[plugin.original || hookName](args); - if (pluginRes != undefined && pluginRes != null) res = res.concat(pluginRes); - } - return res; - }, - - callHookStr: function(hookName, args, sep, pre, post) - { - if (sep == undefined) sep = ''; - if (pre == undefined) pre = ''; - if (post == undefined) post = ''; - var newCallhooks = []; - var callhooks = plugins.callHook(hookName, args); - for (var i = 0, ii = callhooks.length; i < ii; i++) { - newCallhooks[i] = pre + callhooks[i] + post; - } - return newCallhooks.join(sep || ""); - } -}; - -exports.plugins = plugins; diff --git a/static/js/prefixfree.js b/static/js/prefixfree.js deleted file mode 100644 index 1f62dc34..00000000 --- a/static/js/prefixfree.js +++ /dev/null @@ -1,421 +0,0 @@ -/** - * StyleFix 1.0.1 - * @author Lea Verou - * MIT license - */ - -(function(){ - -if(!window.addEventListener) { - return; -} - -var self = window.StyleFix = { - link: function(link) { - try { - // Ignore stylesheets with data-noprefix attribute as well as alternate stylesheets - if(link.rel !== 'stylesheet' || !link.sheet.cssRules || link.hasAttribute('data-noprefix')) { - return; - } - } - catch(e) { - return; - } - if(link.href == "data:text/css,"){ - return false; - } - var url = link.href || link.getAttribute('data-href'), - base = url.replace(/[^\/]+$/, ''), - parent = link.parentNode, - xhr = new XMLHttpRequest(); - - xhr.open('GET', url); - - xhr.onreadystatechange = function() { - if(xhr.readyState === 4) { - var css = xhr.responseText; - - if(css && link.parentNode) { - css = self.fix(css, true, link); - - // Convert relative URLs to absolute, if needed - if(base) { - css = css.replace(/url\((?:'|")?(.+?)(?:'|")?\)/gi, function($0, url) { - if(!/^([a-z]{3,10}:|\/|#)/i.test(url)) { // If url not absolute & not a hash - // May contain sequences like /../ and /./ but those DO work - return 'url("' + base + url + '")'; - } - - return $0; - }); - - // behavior URLs shoudn’t be converted (Issue #19) - css = css.replace(RegExp('\\b(behavior:\\s*?url\\(\'?"?)' + base, 'gi'), '$1'); - } - - var style = document.createElement('style'); - style.textContent = css; - style.media = link.media; - style.disabled = link.disabled; - style.setAttribute('data-href', link.getAttribute('href')); - - parent.insertBefore(style, link); - parent.removeChild(link); - } - } - }; - - xhr.send(null); - - link.setAttribute('data-inprogress', ''); - }, - - styleElement: function(style) { - var disabled = style.disabled; - - style.textContent = self.fix(style.textContent, true, style); - - style.disabled = disabled; - }, - - styleAttribute: function(element) { - var css = element.getAttribute('style'); - - css = self.fix(css, false, element); - - element.setAttribute('style', css); - }, - - process: function() { - // Linked stylesheets - $('link[rel="stylesheet"]:not([data-inprogress])').forEach(StyleFix.link); - - // Inline stylesheets - $('style').forEach(StyleFix.styleElement); - - // Inline styles - $('[style]').forEach(StyleFix.styleAttribute); - }, - - register: function(fixer, index) { - (self.fixers = self.fixers || []) - .splice(index === undefined? self.fixers.length : index, 0, fixer); - }, - - fix: function(css, raw) { - for(var i=0; i<self.fixers.length; i++) { - css = self.fixers[i](css, raw) || css; - } - - return css; - }, - - camelCase: function(str) { - return str.replace(/-([a-z])/g, function($0, $1) { return $1.toUpperCase(); }).replace('-',''); - }, - - deCamelCase: function(str) { - return str.replace(/[A-Z]/g, function($0) { return '-' + $0.toLowerCase() }); - } -}; - -/************************************** - * Process styles - **************************************/ -(function(){ - setTimeout(function(){ - $('link[rel="stylesheet"]').forEach(StyleFix.link); - }, 10); - - document.addEventListener('DOMContentLoaded', StyleFix.process, false); -})(); - -function $(expr, con) { - return [].slice.call((con || document).querySelectorAll(expr)); -} - -})(); - -/** - * PrefixFree 1.0.4 - * @author Lea Verou - * MIT license - */ -(function(root, undefined){ - -if(!window.StyleFix || !window.getComputedStyle) { - return; -} - -var self = window.PrefixFree = { - prefixCSS: function(css, raw) { - var prefix = self.prefix; - - function fix(what, before, after, replacement) { - what = self[what]; - - if(what.length) { - var regex = RegExp(before + '(' + what.join('|') + ')' + after, 'gi'); - - css = css.replace(regex, replacement); - } - } - - fix('functions', '(\\s|:|,)', '\\s*\\(', '$1' + prefix + '$2('); - fix('keywords', '(\\s|:)', '(\\s|;|\\}|$)', '$1' + prefix + '$2$3'); - fix('properties', '(^|\\{|\\s|;)', '\\s*:', '$1' + prefix + '$2:'); - - // Prefix properties *inside* values (issue #8) - if (self.properties.length) { - var regex = RegExp('\\b(' + self.properties.join('|') + ')(?!:)', 'gi'); - - fix('valueProperties', '\\b', ':(.+?);', function($0) { - return $0.replace(regex, prefix + "$1") - }); - } - - if(raw) { - fix('selectors', '', '\\b', self.prefixSelector); - fix('atrules', '@', '\\b', '@' + prefix + '$1'); - } - - // Fix double prefixing - css = css.replace(RegExp('-' + prefix, 'g'), '-'); - - return css; - }, - - // Warning: prefixXXX functions prefix no matter what, even if the XXX is supported prefix-less - prefixSelector: function(selector) { - return selector.replace(/^:{1,2}/, function($0) { return $0 + self.prefix }) - }, - - prefixProperty: function(property, camelCase) { - var prefixed = self.prefix + property; - - return camelCase? StyleFix.camelCase(prefixed) : prefixed; - } -}; - -/************************************** - * Properties - **************************************/ -(function() { - var prefixes = {}, - properties = [], - shorthands = {}, - style = getComputedStyle(document.documentElement, null), - dummy = document.createElement('div').style; - - // Why are we doing this instead of iterating over properties in a .style object? Cause Webkit won't iterate over those. - var iterate = function(property) { - if(property.charAt(0) === '-') { - properties.push(property); - - var parts = property.split('-'), - prefix = parts[1]; - - // Count prefix uses - prefixes[prefix] = ++prefixes[prefix] || 1; - - // This helps determining shorthands - while(parts.length > 3) { - parts.pop(); - - var shorthand = parts.join('-'); - - if(supported(shorthand) && properties.indexOf(shorthand) === -1) { - properties.push(shorthand); - } - } - } - }, - supported = function(property) { - return StyleFix.camelCase(property) in dummy; - } - - // Some browsers have numerical indices for the properties, some don't - if(style.length > 0) { - for(var i=0; i<style.length; i++) { - iterate(style[i]) - } - } - else { - for(var property in style) { - iterate(StyleFix.deCamelCase(property)); - } - } - - // Find most frequently used prefix - var highest = {uses:0}; - for(var prefix in prefixes) { - var uses = prefixes[prefix]; - - if(highest.uses < uses) { - highest = {prefix: prefix, uses: uses}; - } - } - - self.prefix = '-' + highest.prefix + '-'; - self.Prefix = StyleFix.camelCase(self.prefix); - - self.properties = []; - - // Get properties ONLY supported with a prefix - for(var i=0; i<properties.length; i++) { - var property = properties[i]; - - if(property.indexOf(self.prefix) === 0) { // we might have multiple prefixes, like Opera - var unprefixed = property.slice(self.prefix.length); - - if(!supported(unprefixed)) { - self.properties.push(unprefixed); - } - } - } - - // IE fix - if(self.Prefix == 'Ms' - && !('transform' in dummy) - && !('MsTransform' in dummy) - && ('msTransform' in dummy)) { - self.properties.push('transform', 'transform-origin'); - } - - self.properties.sort(); -})(); - -/************************************** - * Values - **************************************/ -(function() { -// Values that might need prefixing -var functions = { - 'linear-gradient': { - property: 'backgroundImage', - params: 'red, teal' - }, - 'calc': { - property: 'width', - params: '1px + 5%' - }, - 'element': { - property: 'backgroundImage', - params: '#foo' - } -}; - - -functions['repeating-linear-gradient'] = -functions['repeating-radial-gradient'] = -functions['radial-gradient'] = -functions['linear-gradient']; - -var keywords = { - 'initial': 'color', - 'zoom-in': 'cursor', - 'zoom-out': 'cursor', - 'box': 'display', - 'flexbox': 'display', - 'inline-flexbox': 'display' -}; - -self.functions = []; -self.keywords = []; - -var style = document.createElement('div').style; - -function supported(value, property) { - style[property] = ''; - style[property] = value; - - return !!style[property]; -} - -for (var func in functions) { - var test = functions[func], - property = test.property, - value = func + '(' + test.params + ')'; - - if (!supported(value, property) - && supported(self.prefix + value, property)) { - // It's supported, but with a prefix - self.functions.push(func); - } -} - -for (var keyword in keywords) { - var property = keywords[keyword]; - - if (!supported(keyword, property) - && supported(self.prefix + keyword, property)) { - // It's supported, but with a prefix - self.keywords.push(keyword); - } -} - -})(); - -/************************************** - * Selectors and @-rules - **************************************/ -(function() { - -var -selectors = { - ':read-only': null, - ':read-write': null, - ':any-link': null, - '::selection': null -}, - -atrules = { - 'keyframes': 'name', - 'viewport': null, - 'document': 'regexp(".")' -}; - -self.selectors = []; -self.atrules = []; - -var style = root.appendChild(document.createElement('style')); - -function supported(selector) { - style.textContent = selector + '{}'; // Safari 4 has issues with style.innerHTML - - return !!style.sheet.cssRules.length; -} - -for(var selector in selectors) { - var test = selector + (selectors[selector]? '(' + selectors[selector] + ')' : ''); - - if(!supported(test) && supported(self.prefixSelector(test))) { - self.selectors.push(selector); - } -} - -for(var atrule in atrules) { - var test = atrule + ' ' + (atrules[atrule] || ''); - - if(!supported('@' + test) && supported('@' + self.prefix + test)) { - self.atrules.push(atrule); - } -} - -root.removeChild(style); - -})(); - -// Properties that accept properties as their value -self.valueProperties = [ - 'transition', - 'transition-property' -] - -// Add class for current prefix -root.className += ' ' + self.prefix; - -StyleFix.register(self.prefixCSS); - - -})(document.documentElement); diff --git a/static/pad.html b/static/pad.html deleted file mode 100644 index 4c6d4d8c..00000000 --- a/static/pad.html +++ /dev/null @@ -1,279 +0,0 @@ -<!doctype html> -<html> - - <title>Etherpad Lite</title> - - <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 href="../static/css/pad.css" rel="stylesheet"> - <link href="../static/custom/pad.css" rel="stylesheet"> - <style title="dynamicsyntax"></style> - - <!-- head and body had been removed intentionally --> - - <div id="editbar"> - <ul id="menu_left"> - <li id="bold" onClick="window.pad&&pad.editbarClick('bold');return false"> - <a class="buttonicon buttonicon-bold" title="Bold (ctrl-B)"></a> - </li> - <li id="italic" onClick="window.pad&&pad.editbarClick('italic'); return false;"> - <a class="buttonicon buttonicon-italic" title="Italics (ctrl-I)"></a> - </li> - <li id="underline" onClick="window.pad&&pad.editbarClick('underline');return false;" > - <a class="buttonicon buttonicon-underline" title="Underline (ctrl-U)"></a> - </li> - <li id="strikethrough" onClick="window.pad&&pad.editbarClick('strikethrough');return false;"> - <a class="buttonicon buttonicon-strikethrough" title="Strikethrough"></a> - </li> - <li class="separator"></li> - <li id="oderedlist" onClick="window.pad&&pad.editbarClick('insertorderedlist');return false;"> - <a class="buttonicon buttonicon-insertorderedlist" title="Toggle Ordered List"></a> - </li> - <li id="unoderedlist" onClick="window.pad&&pad.editbarClick('insertunorderedlist');return false;"> - <a class="buttonicon buttonicon-insertunorderedlist" title="Toggle Bullet List"></a> - </li> - <li id="indent" onClick="window.pad&&pad.editbarClick('indent');return false;"> - <a class="buttonicon buttonicon-indent" title="Indent"></a> - </li> - <li id="outdent" onClick="window.pad&&pad.editbarClick('outdent');return false;"> - <a class="buttonicon buttonicon-outdent" title="Unindent"></a> - </li> - <li class="separator"></li> - <li id="undo" onClick="window.pad&&pad.editbarClick('undo');return false;"> - <a class="buttonicon buttonicon-undo" title="Undo (ctrl-Z)"></a> - </li> - <li id="redo" onClick="window.pad&&pad.editbarClick('redo');return false;"> - <a class="buttonicon buttonicon-redo" title="Redo (ctrl-Y)"></a> - </li> - <li class="separator"></li> - <li id="clearAuthorship" onClick="window.pad&&pad.editbarClick('clearauthorship');return false;"> - <a class="buttonicon buttonicon-clearauthorship" title="Clear Authorship Colors"></a> - </li> - </ul> - <ul id="menu_right"> - <li id="settingslink" onClick="window.pad&&pad.editbarClick('settings');return false;"> - <a class="buttonicon buttonicon-settings" id="settingslink" title="Settings of this pad"></a> - </li> - <li id="importexportlink" onClick="window.pad&&pad.editbarClick('import_export');return false;"> - <a class="buttonicon buttonicon-import_export" id="exportlink" title="Import/Export from/to different document formats"></a> - </li> - <li id="embedlink" onClick="window.pad&&pad.editbarClick('embed');return false;" > - <a class="buttonicon buttonicon-embed" id="embedlink" title="Share and Embed this pad"></a> - </li> - <li class="separator"></li> - <li id="timesliderlink" onClick="document.location = document.location.pathname+ '/timeslider'"> - <a class="buttonicon buttonicon-history" title="Show the history of this pad"></a> - </li> - <li id="usericon" onClick="window.pad&&pad.editbarClick('showusers');return false;" title="Show connected users"> - <span class="buttonicon buttonicon-showusers" id="usericonback"></span> - <span id="online_count">1</span> - </li> - </ul> - </div> - - <div id="users"> - <div id="connectionstatus"></div> - <div id="myuser"> - <div id="mycolorpicker"> - <div id="colorpicker"></div> - <button id="mycolorpickersave">Save</button> - <button id="mycolorpickercancel">Cancel</button> - <span id="mycolorpickerpreview" class="myswatchboxhoverable"></span> - </div> - <div id="myswatchbox"><div id="myswatch"></div></div> - <div id="myusernameform"><input type="text" id="myusernameedit" disabled="disabled"></div> - <div id="mystatusform"><input type="text" id="mystatusedit" disabled="disabled"></div> - </div> - <div id="otherusers"> - <div id="guestprompts"></div> - <table id="otheruserstable" cellspacing="0" cellpadding="0" border="0"> - <tr><td></td></tr> - </table> - <div id="nootherusers"></div> - </div> - <div id="userlistbuttonarea"></div> - </div> - - <div id="editorcontainerbox"> - <div id="editorcontainer"></div> - <div id="editorloadingbox">Loading...</div> - </div> - - <div id="settingsmenu" class="popup"> - <h1>Pad settings</h1> - <div class="column"> - <h2>My view</h2> - <p> - <input type="checkbox" id="options-stickychat" onClick="chat.stickToScreen();"> - <label for="options-stickychat">Chat always on screen</label> - </p> - <p> - <input type="checkbox" id="options-colorscheck"> - <label for="options-colorscheck">Authorship colors</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> - </p> - </div> - <div class="column"> - <h2>Global view</h2> - <p>Currently nothing.</p> - <p class="note">These options affect everyone viewing this pad.</p> - </div> - </div> - - <div id="importexport" class="popup"> - <div class="column"> - <h2>Import from text file, HTML, PDF, Word, ODT or RTF</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="importformdiv" id="importformsubmitdiv"> - <input type="hidden" name="padId" value="blpmaXT35R"> - <span class="nowrap"> - <input type="submit" name="submit" value="Import Now" disabled="disabled" id="importsubmitinput"> - <img alt="" id="importstatusball" src="../static/img/loading.gif" align="top"> - <img alt="" id="importarrow" src="../static/img/leftarrow.png" align="top"> - </span> - </div> - </form> - </div> - <div class="column"> - <h2>Export current pad as</h2> - <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="exportwordlea" target="_blank" onClick="padimpexp.export2Wordle();return false;" class="exportlink"><div class="exporttype" id="exportwordle">Wordle</div></a> - </div> - </div> - - <div id="embed" class="popup"> - <div id="embedreadonly" class="right"> - <input type="checkbox" id="readonlyinput" onClick="padeditbar.setEmbedLinks();"> - <label for="readonlyinput">Read only</label> - </div> - <h1>Share this pad</h1> - <div id="linkcode"> - <h2>Link</h2> - <input id="linkinput" type="text" value=""> - </div> - <br> - <div id="embedcode"> - <h2>Embed URL</h2> - <input id="embedinput" type="text" value=""> - </div> - <br> - <div id="qrcode"> - <h2>QR code</h2> - <div id="qr_center"><img id="embedreadonlyqr"></div> - </div> - </div> - - <div id="chatthrob"></div> - - <div id="chaticon" title="Open the chat for this pad" onclick="chat.show();return false;"> - <span id="chatlabel">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;">- </a></div> - <div id="chattext" class="authorColors"></div> - <div id="chatinputbox"> - <form> - <input id="chatinput" type="text" maxlength="140"> - </form> - </div> - </div> - - <div id="focusprotector"> </div> - - <div id="modaloverlay"> - <div id="modaloverlay-inner"></div> - </div> - - <div id="mainmodals"> - <div id="connectionbox" class="modaldialog"> - <div id="connectionboxinner" class="modaldialog-inner"> - <div class="connecting">Connecting...</div> - <div class="reconnecting">Reestablishing connection...</div> - <div class="disconnected"> - <h2 class="h2_disconnect">Disconnected.</h2> - <h2 class="h2_userdup">Opened in another window.</h2> - <h2 class="h2_unauth">No Authorization.</h2> - <div id="disconnected_looping"> - <p><b>We're having trouble talking to the EtherPad lite synchronization server.</b> You may be connecting through an incompatible firewall or proxy server.</p> - </div> - <div id="disconnected_initsocketfail"> - <p><b>We were unable to connect to the EtherPad lite synchronization server.</b> This may be due to an incompatibility with your web browser or internet connection.</p> - </div> - <div id="disconnected_userdup"> - <p><b>You seem to have opened this pad in another browser window.</b> If you'd like to use this window instead, you can reconnect.</p> - </div> - <div id="disconnected_unknown"> - <p><b>Lost connection with the EtherPad lite synchronization server.</b> This may be due to a loss of network connectivity.</p> - </div> - <div id="disconnected_slowcommit"> - <p><b>Server not responding.</b> This may be due to network connectivity issues or high load on the server.</p> - </div> - <div id="disconnected_unauth"> - <p>Your browser's credentials or permissions have changed while viewing this pad. Try reconnecting.</p> - </div> - <div id="disconnected_deleted"> - <p>This pad was deleted.</p> - </div> - <div id="reconnect_advise"> - <p>If this continues to happen, please let us know</p> - </div> - <div id="reconnect_form"> - <button id="forcereconnect">Reconnect Now</button> - </div> - </div> - </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> - </div> - - </div> - - <script type="text/javascript" src="../static/js/require-kernel.js"></script> - <script type="text/javascript" src="../socket.io/socket.io.js"></script> - <script type="text/javascript" src="../minified/pad.js?callback=require.define"></script> - <script type="text/javascript" src="../static/custom/pad.js"></script> - <script type="text/javascript"> - var clientVars = {}; - (function () { - require.setRootURI("../minified/"); - require.setGlobalKeyPath("require"); - require('/pad').init(); - - /* TODO: These globals shouldn't exist. */ - pad = require('/pad').pad; - chat = require('/chat').chat; - padeditbar = require('/pad_editbar').padeditbar; - padimpexp = require('/pad_impexp').padimpexp; - }()); - </script> - -</html>
\ No newline at end of file diff --git a/static/timeslider.html b/static/timeslider.html deleted file mode 100644 index c629857a..00000000 --- a/static/timeslider.html +++ /dev/null @@ -1,218 +0,0 @@ -<!doctype html> -<html lang="en"> -<head> - <meta charset="utf-8"> - <meta name="robots" content="noindex, nofollow"> - - <title>Etherpad Lite Timeslider</title> - <link rel="stylesheet" href="../../static/css/pad.css"> - <link rel="stylesheet" href="../../static/css/timeslider.css"> - <link rel="stylesheet" href="../../static/custom/timeslider.css"> - <style type="text/css" title="dynamicsyntax"></style> -</head> - -<body id="padbody" class="timeslider limwidth nonpropad nonprouser"> - <div id="padpage"> - <div id="padtop"> - <div class="topbar"> - <div class="topbarleft"> - <!-- --> - </div> - - <div class="topbarright"> - <!-- --> - </div> - - <div class="topbarcenter"> - <a href="/" class="topbarBrand">Etherpad v1.1</a> <a href="http://etherpad.org" - class="EtherpadLink">Etherpad is</a> <a href="../../static/LICENSE" class= - "Licensing">free software</a> - - <div class="fullscreen" onclick="$('body').toggleClass('maximized');"> - Full screen - </div><a href="javascript:void(0);" onclick= - "$('body').toggleClass('maximized');" class="topbarmaximize" title= - "Toggle maximization"></a> - </div> - - <div class="specialkeyarea"> - <!-- --> - </div> - </div> - <div id="alertbar"> - <div id="servermsg"> - <h3>Server Notice<span id="servermsgdate"><!-- --></span>:</h3><a id= - "hidetopmsg" href="javascript:%20void%20pad.hideServerMessage()" name= - "hidetopmsg">hide</a> - - <p id="servermsgtext"><!-- --></p> - </div> - </div> - - <div id="navigation"></div> - - <div id="docbar" class="menu docbar"> - <table border="0" cellpadding="0" cellspacing="0" width="100%" id="docbartable" - class="docbartable"> - <tr> - <td><img src="../../static/img/roundcorner_left.gif" /></td> - - <td id="docbarpadtitle" class="docbarpadtitle" title= - "Public Pad: Public Pad"><span>Public Pad</span></td> - - <td width="100%"> </td> - - <td><img src="../../static/img/roundcorner_right.gif" /></td> - </tr> - </table> - </div><!-- /docbar --> - </div> - - <div id="timeslider-wrapper"> - <div id="error" style="display: none"> - It looks like you're having connection troubles. <a href= - "/ep/pad/view/test/latest">Reconnect now</a>. - </div> - - <div id="timeslider" unselectable="on" style="display: none"> - <div id="timeslider-left"></div> - - <div id="timeslider-right"></div> - - <div id="timer"></div> - - <div id="timeslider-slider"> - <div id="ui-slider-handle"></div> - - <div id="ui-slider-bar"></div> - </div> - - <div id="playpause_button"> - <div id="playpause_button_icon" class=""></div> - </div> - - <div id="steppers"> - <div class="stepper" id="leftstep"></div> - - <div class="stepper" id="rightstep"></div> - </div> - </div> - </div> - - <!--<div id="rightbars" style="top: 95px;"> - <div id="rightbar"><a href="/ep/pad/view/c6fg9GM51V/latest" id="viewlatest">Viewing latest content</a><br> - <a thref="/ep/pad/view/c6fg9GM51V/rev.%revision%" href="/ep/pad/view/c6fg9GM51V/rev.0" class="tlink">Link to this version</a> - <br><a thref="/ep/pad/view/ro.fw470Orpi4T/rev.%revision%" href="/ep/pad/view/ro.fw470Orpi4T/rev.0" class="tlink">Link to read-only page</a><br><a href="/c6fg9GM51V">Edit this pad</a> - <h2>Download as</h2> - <img src="../../static/img/may09/html.gif"><a thref="/ep/pad/export/c6fg9GM51V/rev.%revision%?format=html" href="/ep/pad/export/c6fg9GM51V/rev.0?format=html" class="tlink">HTML</a><br> - <img src="../../static/img/may09/txt.gif"><a thref="/ep/pad/export/c6fg9GM51V/rev.%revision%?format=txt" href="/ep/pad/export/c6fg9GM51V/rev.0?format=txt" class="tlink">Plain text</a><br> - <img src="../../static/img/may09/doc.gif"><a thref="/ep/pad/export/c6fg9GM51V/rev.%revision%?format=doc" href="/ep/pad/export/c6fg9GM51V/rev.0?format=doc" class="tlink">Microsoft Word</a><br> - <img src="../../static/img/may09/pdf.gif"><a thref="/ep/pad/export/c6fg9GM51V/rev.%revision%?format=pdf" href="/ep/pad/export/c6fg9GM51V/rev.0?format=pdf" class="tlink">PDF</a> - - - </div> - <div id="legend"> - <h2>Authors</h2> - <table cellspacing="0" cellpadding="0" border="0" id="authorstable"><tbody><tr><td style="color:#999; padding-left: 10px" colspan="2">No Authors</td></tr></tbody></table> - </div> - </div>--> - - <div id="padmain"> - <div id="padeditor"> - <div id="editbar" class="editbar disabledtoolbar"> - <div id="editbarinner" class="editbarinner"> - <div id="editbarleft" class="editbarleft"> - <!-- --> - </div> - - <div id="editbarright" class="editbarright"> - <!-- termporary place holder--> - <ul> - <li onClick="window.padeditbar.toolbarClick('import_export');return false;"> - <a id="exportlink" title="Export to different document formats"> - <div class="buttonicon buttonicon-import_export"></div> - </a> - </li> - </ul> - <a id = "returnbutton">Return to pad</a> - </div> - - <div id="editbarinner" class="editbarinner"> - <table cellpadding="0" cellspacing="0" border="0" id="editbartable" class= - "editbartable"> - <tr> - <td> - <h1> - <span id="revision_label"></span> - <span id="revision_date"></span> - </h1> - </td> - - <td width="100%"> </td> - </tr> - </table> - - <table cellpadding="0" cellspacing="0" border="0" id="editbarsavetable" - class="editbarsavetable"> - <tr> - <td></td> - </tr> - </table> - </div> - </div> - </div> - - <div id="editorcontainerbox"> - <div id="padcontent"> - - </div> - </div> - </div><!-- /padeditor --> - </div><!-- /padmain --> - </div><!-- /padpage --> - - <div id="modaloverlay"> - <div id="modaloverlay-inner"> - <!-- --> - </div> - </div> - - <div id="mainmodals"></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> - <a id="exportwordlea" target="_blank" onClick="padimpexp.export2Wordle();return false;" class="exportlink"><div class="exporttype" id="exportwordle">Wordle</div></a> - <form id="wordlepost" name="wall" action="http://wordle.net/advanced" method="POST" style="margin-left:0px;"> - <div id="hidetext" style=""><textarea id="text" name="text" id="text" style="display:none;">Coming soon!</textarea></div> - </form> -</div> -</div> - -<script type="text/javascript" src="../../static/js/require-kernel.js"></script> -<script type="text/javascript" src="../../socket.io/socket.io.js"></script> -<script type="text/javascript" src="../../minified/timeslider.js?callback=require.define"></script> -<script type="text/javascript" src="../../static/custom/timeslider.js"></script> -<script type="text/javascript" > - var clientVars = {}; - (function () { - require.setRootURI("../minified/"); - require.setGlobalKeyPath("require"); - require('/timeslider').init(); - - /* TODO: These globals shouldn't exist. */ - padeditbar = require('/pad_editbar').padeditbar; - padimpexp = require('/pad_impexp').padimpexp; - })(); -</script> -</body> -</html> - |