From ce578f69c518c038b523b3aacd3227a6e9415b92 Mon Sep 17 00:00:00 2001 From: Mark Hammond Date: Wed, 1 Sep 2010 17:41:39 +1000 Subject: [PATCH] linkdrop --- .hgignore | 10 + MANIFEST.in | 3 + README.txt | 19 + development.ini | 104 +++ docs/index.txt | 19 + ez_setup.py | 276 +++++++ linkdrop/__init__.py | 0 linkdrop/config/__init__.py | 0 linkdrop/config/deployment.ini_tmpl | 63 ++ linkdrop/config/environment.py | 43 + linkdrop/config/middleware.py | 67 ++ linkdrop/config/routing.py | 28 + linkdrop/controllers/__init__.py | 0 linkdrop/controllers/account.py | 246 ++++++ linkdrop/controllers/error.py | 44 ++ linkdrop/lib/__init__.py | 0 linkdrop/lib/app_globals.py | 18 + linkdrop/lib/base.py | 20 + linkdrop/lib/helpers.py | 163 ++++ linkdrop/model/__init__.py | 9 + linkdrop/model/account.py | 17 + linkdrop/model/expando_mixin.py | 81 ++ linkdrop/model/meta.py | 22 + linkdrop/model/serializer_mixin.py | 40 + linkdrop/model/types.py | 94 +++ linkdrop/public/bg.png | Bin 0 -> 339 bytes linkdrop/public/favicon.ico | Bin 0 -> 2862 bytes linkdrop/public/index.html | 137 ++++ linkdrop/public/pylons-logo.gif | Bin 0 -> 2399 bytes linkdrop/simple_oauth.py | 401 ++++++++++ linkdrop/tests/__init__.py | 34 + linkdrop/tests/functional/__init__.py | 0 linkdrop/tests/test_models.py | 0 linkdrop/websetup.py | 18 + setup.cfg | 31 + setup.py | 37 + test.ini | 21 + web/scratch/README.txt | 6 + web/scratch/oauth/index.html | 168 ++++ web/scratch/oauth/index.js | 121 +++ web/scripts/blade/defer.js | 99 +++ web/scripts/blade/dispatch.js | 227 ++++++ web/scripts/blade/fn.js | 58 ++ web/scripts/blade/jig.js | 863 +++++++++++++++++++++ web/scripts/blade/object.js | 128 +++ web/scripts/blade/url.js | 59 ++ web/scripts/cards.js | 149 ++++ web/scripts/fancyzoom.js | 174 +++++ web/scripts/friendly.js | 129 +++ web/scripts/hashDispatch.js | 42 + web/scripts/iscroll-min.js | 1 + web/scripts/isoDate.js | 149 ++++ web/scripts/jquery.easing.1.3.js | 205 +++++ web/scripts/jquery.masonry.js | 308 ++++++++ web/scripts/jquery.tmpl.js | 486 ++++++++++++ web/scripts/jquery.vgrid.0.1.5.js | 334 ++++++++ web/scripts/json2.js | 7 + web/scripts/md5.js | 381 +++++++++ web/scripts/placeholder.js | 104 +++ web/scripts/rdapi.js | 221 ++++++ web/scripts/requireplugins-jquery-1.4.2.js | 204 +++++ web/scripts/templates/cardsHeader.html | 8 + 62 files changed, 6696 insertions(+) create mode 100644 .hgignore create mode 100644 MANIFEST.in create mode 100644 README.txt create mode 100644 development.ini create mode 100644 docs/index.txt create mode 100644 ez_setup.py create mode 100644 linkdrop/__init__.py create mode 100644 linkdrop/config/__init__.py create mode 100644 linkdrop/config/deployment.ini_tmpl create mode 100644 linkdrop/config/environment.py create mode 100644 linkdrop/config/middleware.py create mode 100644 linkdrop/config/routing.py create mode 100644 linkdrop/controllers/__init__.py create mode 100644 linkdrop/controllers/account.py create mode 100644 linkdrop/controllers/error.py create mode 100644 linkdrop/lib/__init__.py create mode 100644 linkdrop/lib/app_globals.py create mode 100644 linkdrop/lib/base.py create mode 100644 linkdrop/lib/helpers.py create mode 100644 linkdrop/model/__init__.py create mode 100644 linkdrop/model/account.py create mode 100644 linkdrop/model/expando_mixin.py create mode 100644 linkdrop/model/meta.py create mode 100644 linkdrop/model/serializer_mixin.py create mode 100644 linkdrop/model/types.py create mode 100644 linkdrop/public/bg.png create mode 100644 linkdrop/public/favicon.ico create mode 100644 linkdrop/public/index.html create mode 100644 linkdrop/public/pylons-logo.gif create mode 100644 linkdrop/simple_oauth.py create mode 100644 linkdrop/tests/__init__.py create mode 100644 linkdrop/tests/functional/__init__.py create mode 100644 linkdrop/tests/test_models.py create mode 100644 linkdrop/websetup.py create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 test.ini create mode 100644 web/scratch/README.txt create mode 100644 web/scratch/oauth/index.html create mode 100644 web/scratch/oauth/index.js create mode 100644 web/scripts/blade/defer.js create mode 100644 web/scripts/blade/dispatch.js create mode 100644 web/scripts/blade/fn.js create mode 100644 web/scripts/blade/jig.js create mode 100644 web/scripts/blade/object.js create mode 100644 web/scripts/blade/url.js create mode 100644 web/scripts/cards.js create mode 100644 web/scripts/fancyzoom.js create mode 100644 web/scripts/friendly.js create mode 100644 web/scripts/hashDispatch.js create mode 100644 web/scripts/iscroll-min.js create mode 100644 web/scripts/isoDate.js create mode 100644 web/scripts/jquery.easing.1.3.js create mode 100644 web/scripts/jquery.masonry.js create mode 100644 web/scripts/jquery.tmpl.js create mode 100644 web/scripts/jquery.vgrid.0.1.5.js create mode 100644 web/scripts/json2.js create mode 100644 web/scripts/md5.js create mode 100644 web/scripts/placeholder.js create mode 100644 web/scripts/rdapi.js create mode 100644 web/scripts/requireplugins-jquery-1.4.2.js create mode 100644 web/scripts/templates/cardsHeader.html diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..5ab5ad3 --- /dev/null +++ b/.hgignore @@ -0,0 +1,10 @@ +syntax:regexp +\.egg +\.egg-info +\.pyc +\.pyo +\.swp +linkdrop.kpf +renv +\.DS_Store +development.db diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..cdb92b8 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include linkdrop/config/deployment.ini_tmpl +recursive-include linkdrop/public * +recursive-include linkdrop/templates * diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..478b9b3 --- /dev/null +++ b/README.txt @@ -0,0 +1,19 @@ +This file is for you to describe the linkdrop application. Typically +you would include information such as the information below: + +Installation and Setup +====================== + +Install ``linkdrop`` using easy_install:: + + easy_install linkdrop + +Make a config file as follows:: + + paster make-config linkdrop config.ini + +Tweak the config file as appropriate and then setup the application:: + + paster setup-app config.ini + +Then you are ready to go. diff --git a/development.ini b/development.ini new file mode 100644 index 0000000..39e1a6d --- /dev/null +++ b/development.ini @@ -0,0 +1,104 @@ +# +# linkdrop - Pylons development environment configuration +# +# The %(here)s variable will be replaced with the parent directory of this file +# +[DEFAULT] +debug = true +# Uncomment and replace with the address which should receive any error reports +#email_to = you@yourdomain.com +smtp_server = localhost +error_email_from = paste@localhost + +oauth.twitter.com.consumer_key = 2r1qbed58DAaNMe142msTg +oauth.twitter.com.consumer_secret = prh6A961516mJ3XEjd7eERsGxuVZqycrBB6lV7LQ +# This is a 'raindrop' app currently owned by markh. By necessity it is +# configured to use a redirect URL back to the default host and port specified +# below for this server. +oauth.facebook.com.app_id = 158102624846 +oauth.facebook.com.app_secret = 4203f7f23803f405e06509ec4d4b9729 + + +[composite:main] +use = egg:Paste#urlmap +/ = home +/api = api + +[server:main] +use = egg:Paste#http +host = 127.0.0.1 +port = 5000 + + +[app:home] +use = egg:Paste#static +document_root = %(here)s/web + +[app:api] +#use: config:api.ini + +use = egg:linkdrop +full_stack = true +static_files = true + +cache_dir = %(here)s/data +beaker.session.key = linkdrop +beaker.session.secret = somesecret + +# If you'd like to fine-tune the individual locations of the cache data dirs +# for the Cache data, or the Session saves, un-comment the desired settings +# here: +#beaker.cache.data_dir = %(here)s/data/cache +#beaker.session.data_dir = %(here)s/data/sessions + +# SQLAlchemy database URL +sqlalchemy.url = sqlite:///%(here)s/development.db + +# WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* +# Debug mode will enable the interactive debugging tool, allowing ANYONE to +# execute malicious code after an exception is raised. +#set debug = false + + +# Logging configuration +[loggers] +keys = root, routes, linkdrop, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_routes] +level = INFO +handlers = +qualname = routes.middleware +# "level = DEBUG" logs the route matched and routing variables. + +[logger_linkdrop] +level = DEBUG +handlers = +qualname = linkdrop + +[logger_sqlalchemy] +level = INFO +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s,%(msecs)03d %(levelname)-5.5s [%(name)s] [%(threadName)s] %(message)s +datefmt = %H:%M:%S diff --git a/docs/index.txt b/docs/index.txt new file mode 100644 index 0000000..d8932ae --- /dev/null +++ b/docs/index.txt @@ -0,0 +1,19 @@ +linkdrop +++++++++ + +This is the main index page of your documentation. It should be written in +`reStructuredText format `_. + +You can generate your documentation in HTML format by running this command:: + + setup.py pudge + +For this to work you will need to download and install `buildutils`_, +`pudge`_, and `pygments`_. The ``pudge`` command is disabled by +default; to ativate it in your project, run:: + + setup.py addcommand -p buildutils.pudge_command + +.. _buildutils: http://pypi.python.org/pypi/buildutils +.. _pudge: http://pudge.lesscode.org/ +.. _pygments: http://pygments.org/ diff --git a/ez_setup.py b/ez_setup.py new file mode 100644 index 0000000..d24e845 --- /dev/null +++ b/ez_setup.py @@ -0,0 +1,276 @@ +#!python +"""Bootstrap setuptools installation + +If you want to use setuptools in your package's setup.py, just include this +file in the same directory with it, and add this to the top of your setup.py:: + + from ez_setup import use_setuptools + use_setuptools() + +If you want to require a specific version of setuptools, set a download +mirror, or use an alternate download directory, you can do so by supplying +the appropriate options to ``use_setuptools()``. + +This file can also be run as a script to install or upgrade setuptools. +""" +import sys +DEFAULT_VERSION = "0.6c9" +DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3] + +md5_data = { + 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca', + 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb', + 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b', + 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a', + 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618', + 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac', + 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5', + 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4', + 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c', + 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b', + 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27', + 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277', + 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa', + 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e', + 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e', + 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f', + 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2', + 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc', + 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167', + 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64', + 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d', + 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20', + 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab', + 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53', + 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2', + 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e', + 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372', + 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902', + 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de', + 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b', + 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03', + 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a', + 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6', + 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a', +} + +import sys, os +try: from hashlib import md5 +except ImportError: from md5 import md5 + +def _validate_md5(egg_name, data): + if egg_name in md5_data: + digest = md5(data).hexdigest() + if digest != md5_data[egg_name]: + print >>sys.stderr, ( + "md5 validation of %s failed! (Possible download problem?)" + % egg_name + ) + sys.exit(2) + return data + +def use_setuptools( + version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, + download_delay=15 +): + """Automatically find/download setuptools and make it available on sys.path + + `version` should be a valid setuptools version number that is available + as an egg for download under the `download_base` URL (which should end with + a '/'). `to_dir` is the directory where setuptools will be downloaded, if + it is not already available. If `download_delay` is specified, it should + be the number of seconds that will be paused before initiating a download, + should one be required. If an older version of setuptools is installed, + this routine will print a message to ``sys.stderr`` and raise SystemExit in + an attempt to abort the calling script. + """ + was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules + def do_download(): + egg = download_setuptools(version, download_base, to_dir, download_delay) + sys.path.insert(0, egg) + import setuptools; setuptools.bootstrap_install_from = egg + try: + import pkg_resources + except ImportError: + return do_download() + try: + pkg_resources.require("setuptools>="+version); return + except pkg_resources.VersionConflict, e: + if was_imported: + print >>sys.stderr, ( + "The required version of setuptools (>=%s) is not available, and\n" + "can't be installed while this script is running. Please install\n" + " a more recent version first, using 'easy_install -U setuptools'." + "\n\n(Currently using %r)" + ) % (version, e.args[0]) + sys.exit(2) + else: + del pkg_resources, sys.modules['pkg_resources'] # reload ok + return do_download() + except pkg_resources.DistributionNotFound: + return do_download() + +def download_setuptools( + version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, + delay = 15 +): + """Download setuptools from a specified location and return its filename + + `version` should be a valid setuptools version number that is available + as an egg for download under the `download_base` URL (which should end + with a '/'). `to_dir` is the directory where the egg will be downloaded. + `delay` is the number of seconds to pause before an actual download attempt. + """ + import urllib2, shutil + egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3]) + url = download_base + egg_name + saveto = os.path.join(to_dir, egg_name) + src = dst = None + if not os.path.exists(saveto): # Avoid repeated downloads + try: + from distutils import log + if delay: + log.warn(""" +--------------------------------------------------------------------------- +This script requires setuptools version %s to run (even to display +help). I will attempt to download it for you (from +%s), but +you may need to enable firewall access for this script first. +I will start the download in %d seconds. + +(Note: if this machine does not have network access, please obtain the file + + %s + +and place it in this directory before rerunning this script.) +---------------------------------------------------------------------------""", + version, download_base, delay, url + ); from time import sleep; sleep(delay) + log.warn("Downloading %s", url) + src = urllib2.urlopen(url) + # Read/write all in one block, so we don't create a corrupt file + # if the download is interrupted. + data = _validate_md5(egg_name, src.read()) + dst = open(saveto,"wb"); dst.write(data) + finally: + if src: src.close() + if dst: dst.close() + return os.path.realpath(saveto) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +def main(argv, version=DEFAULT_VERSION): + """Install or upgrade setuptools and EasyInstall""" + try: + import setuptools + except ImportError: + egg = None + try: + egg = download_setuptools(version, delay=0) + sys.path.insert(0,egg) + from setuptools.command.easy_install import main + return main(list(argv)+[egg]) # we're done here + finally: + if egg and os.path.exists(egg): + os.unlink(egg) + else: + if setuptools.__version__ == '0.0.1': + print >>sys.stderr, ( + "You have an obsolete version of setuptools installed. Please\n" + "remove it from your system entirely before rerunning this script." + ) + sys.exit(2) + + req = "setuptools>="+version + import pkg_resources + try: + pkg_resources.require(req) + except pkg_resources.VersionConflict: + try: + from setuptools.command.easy_install import main + except ImportError: + from easy_install import main + main(list(argv)+[download_setuptools(delay=0)]) + sys.exit(0) # try to force an exit + else: + if argv: + from setuptools.command.easy_install import main + main(argv) + else: + print "Setuptools version",version,"or greater has been installed." + print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)' + +def update_md5(filenames): + """Update our built-in md5 registry""" + + import re + + for name in filenames: + base = os.path.basename(name) + f = open(name,'rb') + md5_data[base] = md5(f.read()).hexdigest() + f.close() + + data = [" %r: %r,\n" % it for it in md5_data.items()] + data.sort() + repl = "".join(data) + + import inspect + srcfile = inspect.getsourcefile(sys.modules[__name__]) + f = open(srcfile, 'rb'); src = f.read(); f.close() + + match = re.search("\nmd5_data = {\n([^}]+)}", src) + if not match: + print >>sys.stderr, "Internal error!" + sys.exit(2) + + src = src[:match.start(1)] + repl + src[match.end(1):] + f = open(srcfile,'w') + f.write(src) + f.close() + + +if __name__=='__main__': + if len(sys.argv)>2 and sys.argv[1]=='--md5update': + update_md5(sys.argv[2:]) + else: + main(sys.argv[1:]) + + + + + + diff --git a/linkdrop/__init__.py b/linkdrop/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/linkdrop/config/__init__.py b/linkdrop/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/linkdrop/config/deployment.ini_tmpl b/linkdrop/config/deployment.ini_tmpl new file mode 100644 index 0000000..a5e0343 --- /dev/null +++ b/linkdrop/config/deployment.ini_tmpl @@ -0,0 +1,63 @@ +# +# linkdrop - Pylons configuration +# +# The %(here)s variable will be replaced with the parent directory of this file +# +[DEFAULT] +debug = true +email_to = you@yourdomain.com +smtp_server = localhost +error_email_from = paste@localhost + +[server:main] +use = egg:Paste#http +host = 0.0.0.0 +port = 5000 + +[app:main] +use = egg:linkdrop +full_stack = true +static_files = true + +cache_dir = %(here)s/data +beaker.session.key = linkdrop +beaker.session.secret = ${app_instance_secret} +app_instance_uuid = ${app_instance_uuid} + +# If you'd like to fine-tune the individual locations of the cache data dirs +# for the Cache data, or the Session saves, un-comment the desired settings +# here: +#beaker.cache.data_dir = %(here)s/data/cache +#beaker.session.data_dir = %(here)s/data/sessions + +# SQLAlchemy database URL +sqlalchemy.url = sqlite:///production.db + +# WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* +# Debug mode will enable the interactive debugging tool, allowing ANYONE to +# execute malicious code after an exception is raised. +set debug = false + + +# Logging configuration +[loggers] +keys = root + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s] [%(threadName)s] %(message)s diff --git a/linkdrop/config/environment.py b/linkdrop/config/environment.py new file mode 100644 index 0000000..5ff38fa --- /dev/null +++ b/linkdrop/config/environment.py @@ -0,0 +1,43 @@ +"""Pylons environment configuration""" +import os + +from pylons.configuration import PylonsConfig +from sqlalchemy import engine_from_config + +import linkdrop.lib.app_globals as app_globals +import linkdrop.lib.helpers +from linkdrop.config.routing import make_map +from linkdrop.model import init_model + +def load_environment(global_conf, app_conf): + """Configure the Pylons environment via the ``pylons.config`` + object + """ + config = PylonsConfig() + + # Pylons paths + root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + paths = dict(root=root, + controllers=os.path.join(root, 'controllers'), + static_files=os.path.join(root, 'public'), + templates=[os.path.join(root, 'templates')]) + + # Initialize config with the basic options + config.init_app(global_conf, app_conf, package='linkdrop', paths=paths) + + config['routes.map'] = make_map(config) + config['pylons.app_globals'] = app_globals.Globals(config) + config['pylons.h'] = linkdrop.lib.helpers + + # Setup cache object as early as possible + import pylons + pylons.cache._push_object(config['pylons.app_globals'].cache) + + # Setup the SQLAlchemy database engine + engine = engine_from_config(config, 'sqlalchemy.') + init_model(engine) + + # CONFIGURATION OPTIONS HERE (note: all config options will override + # any Pylons config options) + + return config diff --git a/linkdrop/config/middleware.py b/linkdrop/config/middleware.py new file mode 100644 index 0000000..63622c5 --- /dev/null +++ b/linkdrop/config/middleware.py @@ -0,0 +1,67 @@ +"""Pylons middleware initialization""" +from beaker.middleware import SessionMiddleware +from paste.cascade import Cascade +from paste.registry import RegistryManager +from paste.urlparser import StaticURLParser +from paste.deploy.converters import asbool +from pylons.middleware import ErrorHandler, StatusCodeRedirect +from pylons.wsgiapp import PylonsApp +from routes.middleware import RoutesMiddleware + +from linkdrop.config.environment import load_environment + +def make_app(global_conf, full_stack=True, static_files=True, **app_conf): + """Create a Pylons WSGI application and return it + + ``global_conf`` + The inherited configuration for this application. Normally from + the [DEFAULT] section of the Paste ini file. + + ``full_stack`` + Whether this application provides a full WSGI stack (by default, + meaning it handles its own exceptions and errors). Disable + full_stack when this application is "managed" by another WSGI + middleware. + + ``static_files`` + Whether this application serves its own static files; disable + when another web server is responsible for serving them. + + ``app_conf`` + The application's local configuration. Normally specified in + the [app:] section of the Paste ini file (where + defaults to main). + + """ + # Configure the Pylons environment + config = load_environment(global_conf, app_conf) + + # The Pylons WSGI app + app = PylonsApp(config=config) + + # Routing/Session/Cache Middleware + app = RoutesMiddleware(app, config['routes.map'], singleton=False) + app = SessionMiddleware(app, config) + + # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares) + + if asbool(full_stack): + # Handle Python exceptions + app = ErrorHandler(app, global_conf, **config['pylons.errorware']) + + # Display error documents for 401, 403, 404 status codes (and + # 500 when debug is disabled) + if asbool(config['debug']): + app = StatusCodeRedirect(app) + else: + app = StatusCodeRedirect(app, [400, 401, 403, 404, 500]) + + # Establish the Registry for this application + app = RegistryManager(app) + + if asbool(static_files): + # Serve static files + static_app = StaticURLParser(config['pylons.paths']['static_files']) + app = Cascade([static_app, app]) + app.config = config + return app diff --git a/linkdrop/config/routing.py b/linkdrop/config/routing.py new file mode 100644 index 0000000..c1994d1 --- /dev/null +++ b/linkdrop/config/routing.py @@ -0,0 +1,28 @@ +"""Routes configuration + +The more specific and detailed routes should be defined first so they +may take precedent over the more generic routes. For more information +refer to the routes manual at http://routes.groovie.org/docs/ +""" +from routes import Mapper + +def make_map(config): + """Create, configure and return the routes Mapper""" + map = Mapper(directory=config['pylons.paths']['controllers'], + always_scan=config['debug']) + map.minimization = False + map.explicit = False + + # The ErrorController route (handles 404/500 error pages); it should + # likely stay at the top, ensuring it can always be resolved + map.connect('/error/{action}', controller='error') + map.connect('/error/{action}/{id}', controller='error') + + # CUSTOM ROUTES HERE + map.connect('/account/oauth_facebook/{redirect_info}', controller='account', action="oauth_facebook") + + + map.connect('/{controller}/{action}') + map.connect('/{controller}/{action}/{id}') + + return map diff --git a/linkdrop/controllers/__init__.py b/linkdrop/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/linkdrop/controllers/account.py b/linkdrop/controllers/account.py new file mode 100644 index 0000000..ce175d5 --- /dev/null +++ b/linkdrop/controllers/account.py @@ -0,0 +1,246 @@ +import logging +import urllib, cgi, json + +from pylons import config, request, response, session, tmpl_context as c, url +from pylons.controllers.util import abort, redirect +from pylons.decorators import jsonify +from pylons.decorators.util import get_pylons + +from linkdrop import simple_oauth +from linkdrop.lib.base import BaseController, render +from linkdrop.lib.helpers import json_exception_response, api_response, api_entry, api_arg + +from linkdrop.model.meta import Session +from linkdrop.model.account import Account +from sqlalchemy.orm.exc import NoResultFound +from sqlalchemy import and_ + +log = logging.getLogger(__name__) + + +def get_oauth_config(provider): + key = 'oauth.'+provider+'.' + keylen = len(key) + d = {} + for k,v in config.items(): + if k.startswith(key): + d[k[keylen:]] = v + return d + +def get_oauth_consumer(oauth_config): + return simple_oauth.OAuthEntity(oauth_config['consumer_key'], oauth_config['consumer_secret']) + + +class AccountController(BaseController): + """ +Accounts +======== + +The 'account' namespace is used to access information regarding the current +user's account. This does not retrieve the users contact, for that see +the contacts API that uses @me/@self. + +""" + __api_controller__ = True # for docs + + # for testing... + @api_response + @json_exception_response + def get(self, id=None): + if id is None: + accts = Session.query(Account).all() + else: + accts = [Session.query(Account).get(id)] + return [a.to_dict() for a in accts] + + @json_exception_response + def oauth_start(self, *args, **kw): + pylons = get_pylons(args) + try: + domain = request.params.get('domain') + return_path = request.params['return_to'] + except KeyError, what: + raise ValueError("'%s' request param is not optional" % (what,)) + + scope = request.params.get('scope', domain) + + oauth_config = get_oauth_config(domain) + url_gen = simple_oauth.getOAuthUrlGenerator(domain, '') + + consumer = get_oauth_consumer(oauth_config) + callback_url = url(controller='account', action="oauth_done", + qualified=True, domain=domain, + return_to=return_path) + + csrf_token = request.environ.get('CSRF_TOKEN') + if csrf_token: + callback_url += '&rd-token=%s' % (csrf_token) + + # Note the xoauth module automatically generates nonces and timestamps to prevent replays.) + request_entity = simple_oauth.GenerateRequestToken(consumer, scope, None, None, + callback_url, url_gen) + + # Save the request secret into the session. + session = request.environ['beaker.session'] + session["oauth_request_key"] = request_entity.key + session["oauth_request_secret"] = request_entity.secret + session.save() + + # And arrange for the client to redirect to the service to continue + # the process... + loc = '%s?oauth_token=%s' % (url_gen.GetAuthorizeTokenUrl(), + simple_oauth.UrlEscape(request_entity.key)) + log.info("redirecting to %r and requesting to land back on %r", + loc, callback_url) + pylons.response.headers['Location'] = loc + pylons.response.status_int = 302 + + @json_exception_response + def oauth_done(self, *args, **kw): + pylons = get_pylons(args) + try: + domain = request.params['domain'] + return_path = request.params['return_to'] + except KeyError, what: + raise ValueError("'%s' request param is not optional" % (what,)) + + oauth_config = get_oauth_config(domain) + url_gen = simple_oauth.getOAuthUrlGenerator(domain, '') + + oauth_token = request.params['oauth_token'] + oauth_verifier = request.params['oauth_verifier'] + # Get the request secret from the session. + # Save the request secret into the session. + session = request.environ['beaker.session'] + request_key = session.pop("oauth_request_key") + request_secret = session.pop("oauth_request_secret") + session.save() + + if request_secret and request_key == oauth_token: + request_token = simple_oauth.OAuthEntity(oauth_token, request_secret) + consumer = get_oauth_consumer(oauth_config) + # Make the oauth call to get the final verified token + verified_token = simple_oauth.GetAccessToken(consumer, request_token, + oauth_verifier, url_gen) + + if domain == "twitter.com": + userid = verified_token.user_id + username = verified_token.screen_name + else: + raise ValueError(domain) # can't obtain user information for this provider... + # Find or create an account + try: + acct = Session.query(Account).filter(and_(Account.domain==domain, Account.userid==userid)).one() + except NoResultFound: + acct = Account() + acct.domain = domain + acct.userid = userid + acct.username = username + Session.add(acct) + + # Update the account with the final tokens and delete the transient ones. + acct.oauth_token = verified_token.key + acct.oauth_token_secret = verified_token.secret + + Session.commit() + fragment = "oauth_success_" + domain + else: + fragment = "oauth_failure_" + domain + + # and finally redirect back to the signup page. + loc = request.host_url + return_path + "#" + fragment.replace(".", "_") + log.info("Final redirect back to %r", loc) + pylons.response.headers['Location'] = loc + pylons.response.status_int = 302 + + @json_exception_response + def oauth_facebook(self, redirect_info=None, *args, **kw): + pylons = get_pylons(args) + # NOTE: facebook insists the redirect URLS are identical for each + # leg of the auth (ie, no 'oauth_start/oauth_done') and that the + # redirect URL contains no request params (ie, no '?return_to=xxx' in + # the URL.) We worm around the second problem by encoding the params + # as base64 and appending it to the URL itself (in which case it comes + # to us via redirect_info) + if redirect_info is None: + # this is the initial request from linkdrop. + return_to = request.params.get('return_to', None) + else: + # this is a redirected leg. + return_to = redirect_info.decode("base64") + + domain = "facebook.com" + # experimentation shows callback_url can not have request params! + callback_url = url(controller='account', action="oauth_facebook", + redirect_info=return_to.encode("base64")[:-1], + qualified=True) + csrf_token = request.environ.get('CSRF_TOKEN') + if csrf_token: + if '?' in callback_url: + callback_url += '&rd-token=%s' % (csrf_token) + else: + callback_url += '?rd-token=%s' % (csrf_token) + + oauth_config = get_oauth_config(domain) + + args = dict(client_id=oauth_config['app_id'], redirect_uri=callback_url) + verification_code = request.params.get("code") + if not verification_code: + # make the auth request to get the code. + args['scope'] = request.params.get('scope', '') + loc = "https://graph.facebook.com/oauth/authorize?" + urllib.urlencode(args) + log.info("facebook auth redirecting to %r and requesting to land back on %r", + loc, callback_url) + else: + args["client_secret"] = oauth_config['app_secret'] + args["code"] = verification_code + # must we really use urlopen here, or can we do it via redirects? + resp = urllib.urlopen( + "https://graph.facebook.com/oauth/access_token?" + + urllib.urlencode(args)) + redirect_query = "" + if resp.headers.get("content-type", "").startswith("text/javascript"): + # almost certainly an error response. + resp = json.load(resp) + log.error("facebook auth request failed with %r", resp) + fragment = "oauth_failure_" + domain + else: + response = cgi.parse_qs(resp.read()) + access_token = response["access_token"][-1] + + # Download the user profile and until we know what to do with + # it, just log it! + profile = json.load(urllib.urlopen( + "https://graph.facebook.com/me?" + + urllib.urlencode(dict(access_token=access_token)))) + + from pprint import pformat + log.info("facebook profile: %s", pformat(profile)) + + if 'error' in profile: + log.error("facebook profile request failed with %r", profile) + fragment = "oauth_failure_" + domain + else: + # Setup the linkdrop account. + facebookid = profile['id'] + acct_proto = "facebook" + # Try and find an account to use or create a new one. + try: + acct = Session.query(Account).filter(and_(Account.domain=="facebook.com", Account.userid==facebookid)).one() + except NoResultFound: + acct = Account() + acct.domain = "facebook.com" + acct.userid = facebookid + acct.username = "" + Session.add(acct) + + acct.oauth_token = access_token + Session.commit() + fragment = "oauth_success_" + domain + redirect_query = "?" + urllib.urlencode(dict(id=acct.id, name=profile['name'])) + + loc = request.host_url + return_to + redirect_query + "#" + fragment.replace(".", "_") + log.info("Final redirect back to %r", loc) + + pylons.response.headers['Location'] = loc + pylons.response.status_int = 302 diff --git a/linkdrop/controllers/error.py b/linkdrop/controllers/error.py new file mode 100644 index 0000000..9dd9046 --- /dev/null +++ b/linkdrop/controllers/error.py @@ -0,0 +1,44 @@ +import cgi + +from paste.urlparser import PkgResourcesParser +from pylons.middleware import error_document_template +from webhelpers.html.builder import literal + +from linkdrop.lib.base import BaseController + +class ErrorController(BaseController): + """Generates error documents as and when they are required. + + The ErrorDocuments middleware forwards to ErrorController when error + related status codes are returned from the application. + + This behaviour can be altered by changing the parameters to the + ErrorDocuments middleware in your config/middleware.py file. + + """ + def document(self): + """Render the error document""" + request = self._py_object.request + resp = request.environ.get('pylons.original_response') + content = literal(resp.body) or cgi.escape(request.GET.get('message', '')) + page = error_document_template % \ + dict(prefix=request.environ.get('SCRIPT_NAME', ''), + code=cgi.escape(request.GET.get('code', str(resp.status_int))), + message=content) + return page + + def img(self, id): + """Serve Pylons' stock images""" + return self._serve_file('/'.join(['media/img', id])) + + def style(self, id): + """Serve Pylons' stock stylesheets""" + return self._serve_file('/'.join(['media/style', id])) + + def _serve_file(self, path): + """Call Paste's FileApp (a WSGI application) to serve the file + at the specified path + """ + request = self._py_object.request + request.environ['PATH_INFO'] = '/%s' % path + return PkgResourcesParser('pylons', 'pylons')(request.environ, self.start_response) diff --git a/linkdrop/lib/__init__.py b/linkdrop/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/linkdrop/lib/app_globals.py b/linkdrop/lib/app_globals.py new file mode 100644 index 0000000..b3da474 --- /dev/null +++ b/linkdrop/lib/app_globals.py @@ -0,0 +1,18 @@ +"""The application's Globals object""" + +from beaker.cache import CacheManager +from beaker.util import parse_cache_config_options + +class Globals(object): + """Globals acts as a container for objects available throughout the + life of the application + + """ + + def __init__(self, config): + """One instance of Globals is created during application + initialization and is available during requests via the + 'app_globals' variable + + """ + self.cache = CacheManager(**parse_cache_config_options(config)) diff --git a/linkdrop/lib/base.py b/linkdrop/lib/base.py new file mode 100644 index 0000000..b5b1a99 --- /dev/null +++ b/linkdrop/lib/base.py @@ -0,0 +1,20 @@ +"""The base Controller API + +Provides the BaseController class for subclassing. +""" +from pylons.controllers import WSGIController +from pylons.templating import render_mako as render + +from linkdrop.model.meta import Session + +class BaseController(WSGIController): + + def __call__(self, environ, start_response): + """Invoke the Controller""" + # WSGIController.__call__ dispatches to the Controller method + # the request is routed to. This routing information is + # available in environ['pylons.routes_dict'] + try: + return WSGIController.__call__(self, environ, start_response) + finally: + Session.remove() diff --git a/linkdrop/lib/helpers.py b/linkdrop/lib/helpers.py new file mode 100644 index 0000000..fa24ab1 --- /dev/null +++ b/linkdrop/lib/helpers.py @@ -0,0 +1,163 @@ +"""Helper functions + +Consists of functions to typically be used within templates, but also +available to Controllers. This module is available to templates as 'h'. +""" +from pylons.decorators.util import get_pylons +from pylons.controllers.core import HTTPException +from decorator import decorator +import pprint +from xml.sax.saxutils import escape +import json + +import logging + + +from raindrop.model.meta import Session + +logger=logging.getLogger(__name__) + +@decorator +def exception_rollback(func, *args, **kwargs): + try: + return func(*args, **kwargs) + except Exception, e: + Session.rollback() + raise + +@decorator +def json_exception_response(func, *args, **kwargs): + try: + return func(*args, **kwargs) + except HTTPException: + raise + except Exception, e: + logger.exception("%s(%s, %s) failed", func, args, kwargs) + #pylons = get_pylons(args) + #pylons.response.status_int = 500 + return { + 'result': None, + 'error': { + 'name': e.__class__.__name__, + 'message': str(e) + } + } + +@decorator +def api_response(func, *args, **kwargs): + pylons = get_pylons(args) + data = func(*args, **kwargs) + format = pylons.request.params.get('format', 'json') + + if format == 'test': + pylons.response.headers['Content-Type'] = 'text/plain' + return pprint.pformat(data) + elif format == 'xml': + # a quick-dirty dict serializer + def ser(d): + r = "" + for k,v in d.items(): + if isinstance(v, dict): + r += "<%s>%s" % (k, ser(v), k) + elif isinstance(v, list): + for i in v: + #print k,i + r += ser({k:i}) + else: + r += "<%s>%s" % (k, escape("%s"%v), k) + return r + pylons.response.headers['Content-Type'] = 'text/xml' + return '' + ser({'response': data}).encode('utf-8') + pylons.response.headers['Content-Type'] = 'application/json' + return json.dumps(data) + +def api_entry(**kw): + """Decorator to add tags to functions. + """ + def decorate(f): + if not hasattr(f, "__api"): + f.__api = kw + if not getattr(f, "__doc__") and 'doc' in kw: + doc = kw['doc'] + if 'name' in kw: + doc = kw['name'] + "\n" + "="*len(kw['name']) +"\n\n" + doc + args = [] + for m in kw.get('queryargs', []): + line = " %(name)-20s %(type)-10s %(doc)s" % m + opts = [] + if m['required']: opts.append("required") + if m['default']: opts.append("default=%s" % m['default']) + if m['allowed']: opts.append("options=%r" % m['allowed']) + if opts: + line = "%s (%s)" % (line, ','.join(opts),) + args.append(line) + d = "Request Arguments\n-----------------\n\n%s\n\n" % '\n'.join(args) + if 'bodyargs' in kw: + assert 'body' not in kw, "can't specify body and bodyargs" + for m in kw['bodyargs']: + line = " %(name)-20s %(type)-10s %(doc)s" % m + opts = [] + if m['required']: opts.append("required") + if m['default']: opts.append("default=%s" % m['default']) + if m['allowed']: opts.append("options=%r" % m['allowed']) + if opts: + line = "%s (%s)" % (line, ','.join(opts),) + args.append(line) + d = d+ "**Request Body**: A JSON object with the following fields:" + d = d+ "\n".join(args) + elif 'body' in kw: + d = d+ "**Request Body**: %(type)-10s %(doc)s\n\n" % kw['body'] + if 'response' in kw: + d = d+ "**Response Body**: %(type)-10s %(doc)s\n\n" % kw['response'] + f.__doc__ = doc + d + return f + return decorate + +def api_arg(name, type=None, required=False, default=None, allowed=None, doc=None): + return { + 'name': name, + 'type': type, + 'required': required, + 'default': default, + 'allowed': allowed, + 'doc': doc or '' + } + + +if __name__ == '__main__': + @api_entry( + name="contacts", + body=("json", "A json object"), + doc=""" +See Portable Contacts for api for detailed documentation. + +http://portablecontacts.net/draft-spec.html + +**Examples**:: + + /contacts returns all contacts + /contacts/@{user}/@{group} returns all contacts (user=me, group=all) + /contacts/@{user}/@{group}/{id} returns a specific contact + +""", + urlargs=[ + api_arg('user', 'string', True, None, ['me'], 'User to query'), + api_arg('group', 'string', True, None, ['all', 'self'], 'Group to query'), + api_arg('id', 'integer', False, None, None, 'Contact ID to return'), + ], + queryargs=[ + # name, type, required, default, allowed, doc + api_arg('filterBy', 'string', False, None, None, 'Field name to query'), + api_arg('filterOp', 'string', False, None, ['equals', 'contains', 'startswith', 'present'], 'Filter operation'), + api_arg('filterValue', 'string', False, None, None, 'A value to compare using filterOp (not used with present)'), + api_arg('startIndex', 'int', False, 0, None, 'The start index of the query, used for paging'), + api_arg('count', 'int', False, 20, None, 'The number of results to return, used with paging'), + api_arg('sortBy', 'string', False, 'ascending', ['ascending','descending'], 'A list of conversation ids'), + api_arg('sortOrder', 'string', False, 'ascending', ['ascending','descending'], 'A list of conversation ids'), + api_arg('fields', 'list', False, None, None, 'A list of fields to return'), + ], + response=('object', 'A POCO result object') + ) + def foo(): + pass + print foo.__doc__ diff --git a/linkdrop/model/__init__.py b/linkdrop/model/__init__.py new file mode 100644 index 0000000..2c4277f --- /dev/null +++ b/linkdrop/model/__init__.py @@ -0,0 +1,9 @@ +"""The application's model objects""" +from linkdrop.model.meta import Session, Base +from linkdrop.model.account import Account + + +def init_model(engine): + """Call me before using any of the tables or classes in the model""" + Session.configure(bind=engine) + Base.metadata.create_all(bind=Session.bind) diff --git a/linkdrop/model/account.py b/linkdrop/model/account.py new file mode 100644 index 0000000..3a52cf7 --- /dev/null +++ b/linkdrop/model/account.py @@ -0,0 +1,17 @@ +# Account definitions +from sqlalchemy import Column, Integer, String, Boolean, UniqueConstraint +from linkdrop.model.meta import Base, Session, make_table_args +from linkdrop.model.types import RDUnicode +from linkdrop.model.expando_mixin import JsonExpandoMixin +from linkdrop.model.serializer_mixin import SerializerMixin + +class Account(JsonExpandoMixin, SerializerMixin, Base): + __tablename__ = 'accounts' + __table_args__ = make_table_args(UniqueConstraint('domain', 'username', 'userid')) + + id = Column(Integer, primary_key=True) + + # The external account identity information, modelled from poco + domain = Column(RDUnicode(128), nullable=False) + username = Column(RDUnicode(128), nullable=False) + userid = Column(RDUnicode(128), nullable=False) diff --git a/linkdrop/model/expando_mixin.py b/linkdrop/model/expando_mixin.py new file mode 100644 index 0000000..4e7a88b --- /dev/null +++ b/linkdrop/model/expando_mixin.py @@ -0,0 +1,81 @@ +# A mixin for all objects which want 'expando' functionality; +# ie, the ability to have arbitrary content stored in a json column, but +# have the object seamlessly provide access to the items in the json as though +# they were real properties. + +import json +from sqlalchemy.orm.interfaces import MapperExtension, EXT_CONTINUE +from sqlalchemy import Column, Text + +# A mapper extension to help us with 'expandos' magic - ensures that expando +# attributes set via normal 'object.expando=value' syntax is reflected +# back into the json_attributes column. +class _ExpandoFlushingExtension(MapperExtension): + def before_insert(self, mapper, connection, instance): + instance._flush_expandos() + return EXT_CONTINUE + + before_update = before_insert + + +# The actual mixin class +class JsonExpandoMixin(object): + __mapper_args__ = {'extension': _ExpandoFlushingExtension()} + json_attributes = Column(Text) + + # Methods for providing 'expandos' via the json_attributes field. + def _get_expando_namespace(self): + if '_expando_namespace' not in self.__dict__: + assert '_orig_json' not in self.__dict__ + attrs = self.json_attributes + self.__dict__['_orig_json'] = attrs + if not attrs: + _expando_namespace = {} + else: + _expando_namespace = json.loads(attrs) + self.__dict__['_expando_namespace'] = _expando_namespace + return self.__dict__['_expando_namespace'] + + def __getattr__(self, name): + if name.startswith('_'): + raise AttributeError(name) + # is it in the namespace? + try: + return self._get_expando_namespace()[name] + except KeyError: + raise AttributeError(name) + + def __setattr__(self, name, value): + if name.startswith("_") or name in self.__dict__ or hasattr(self.__class__, name): + object.__setattr__(self, name, value) + return + # assume it is an 'expando' object + # Set json attributes to itself simply so the object is marked as + # 'dirty' for subsequent updates. + self.json_attributes = self.json_attributes + self._get_expando_namespace()[name] = value + + def __delattr__(self, name): + try: + del self._get_expando_namespace()[name] + self.json_attributes = self.json_attributes # to mark as dirty + except KeyError: + raise AttributeError("'%s' is not an 'expando' property" % (name,)) + + # Note that you should never need to call this function manually - a + # mapper extension is defined above which calls this function before + # the object is saved. + def _flush_expandos(self): + try: + en = self.__dict__['_expando_namespace'] + except KeyError: + # no property accesses at all + return + if self._orig_json != self.json_attributes: + # This means someone used 'expandos' *and* explicitly set + # json_attributes on the same object. + raise ValueError("object's json_attributes have changed externally") + self.json_attributes = None if not en else json.dumps(en) + # and reset the world back to as if expandos have never been set. + del self.__dict__['_orig_json'] + del self.__dict__['_expando_namespace'] diff --git a/linkdrop/model/meta.py b/linkdrop/model/meta.py new file mode 100644 index 0000000..15f04d6 --- /dev/null +++ b/linkdrop/model/meta.py @@ -0,0 +1,22 @@ +"""SQLAlchemy Metadata and Session object""" +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import scoped_session, sessionmaker + +__all__ = ['Base', 'Session'] + +# SQLAlchemy session manager. Updated by model.init_model() +Session = scoped_session(sessionmaker()) + +# The declarative Base +Base = declarative_base() + + +# Return a value suitable for __table_args__ which includes common table +# arguments which should be used by all tables. +def make_table_args(*args, **kw): + kwuse = kw.copy() + if 'mysql_charset' not in kwuse: + kwuse['mysql_charset'] = 'utf8' + if not args: + return kwuse + return args + (kwuse,) diff --git a/linkdrop/model/serializer_mixin.py b/linkdrop/model/serializer_mixin.py new file mode 100644 index 0000000..8afd722 --- /dev/null +++ b/linkdrop/model/serializer_mixin.py @@ -0,0 +1,40 @@ +# A serializer for items. +from datetime import datetime +import json +from sqlalchemy.orm.util import object_mapper +from sqlalchemy.orm.properties import ColumnProperty, RelationProperty + +class SerializerMixin(object): + # deal with the special plural fields + def _rd_collection_to_dict(self, name, fields): + if fields and name not in fields: + return + if hasattr(self, name): + for entry in getattr(self, name, []): + val = entry.to_dict() + if val: + #import sys;print >> sys.stderr, val + yield val + #propdict.setdefault(name, []).append(val) + + def to_dict(self, fields=None): + propdict = {} + for prop in object_mapper(self).iterate_properties: + if isinstance(prop, (ColumnProperty)):# or isinstance(prop, RelationProperty) and prop.secondary: + if fields and prop.key not in fields: continue + val = getattr(self, prop.key) + if val: + if isinstance(val, datetime): + val = val.isoformat().split('.')[0].replace('+00:00','Z') + + if prop.key == 'json_attributes': + propdict.update(json.loads(val)) + else: + propdict[prop.key] = val + elif prop.key != 'json_attributes': + propdict[prop.key] = val + + for val in self._rd_collection_to_dict('tags', fields): + propdict.setdefault('tags', []).append(val) + + return propdict diff --git a/linkdrop/model/types.py b/linkdrop/model/types.py new file mode 100644 index 0000000..d989852 --- /dev/null +++ b/linkdrop/model/types.py @@ -0,0 +1,94 @@ +import time +import datetime +import dateutil.parser +from dateutil.tz import tzutc, tzlocal +from email.utils import formatdate as email_format_date +from email.utils import mktime_tz, parsedate_tz +from sqlalchemy.types import TypeDecorator, DateTime, Unicode +import codecs + +# SqlAlchemy takes a very anal approach to Unicode - if a column is unicode, +# then the Python object must also be unicode and not a string. This is very +# painful in py2k, so we loosen this a little - string objects are fine so long +# as they don't include extended chars. +class RDUnicode(Unicode): + def __init__(self, length=None, **kwargs): + kwargs.setdefault('_warn_on_bytestring', False) + super(RDUnicode, self).__init__(length=length, **kwargs) + + def bind_processor(self, dialect): + encoder = codecs.getencoder(dialect.encoding) + def process(value): + if isinstance(value, unicode): + return encoder.encode(value) + elif isinstance(value, str): + # Force an error should someone pass a non-ascii string + assert value.decode('ascii')==value + return value + elif value is None: + return None + raise ValueError("invalid value for unicode column: %r" % (value,)) + + +# from http://stackoverflow.com/questions/2528189/can-sqlalchemy-datetime-objects-only-be-naive +# to force all dates going to and coming back from the DB to be in UTC. +# All raindrop DateTime fields should be declared using this type. +class UTCDateTime(TypeDecorator): + impl = DateTime + + def process_bind_param(self, value, engine): + if value is not None: + try: + return value.astimezone(tzutc()) + except ValueError: + print "FAILED", value + + def process_result_value(self, value, engine): + if value is not None: + return datetime.datetime(value.year, value.month, value.day, + value.hour, value.minute, value.second, + value.microsecond, tzinfo=tzutc()) + + # Helpers for creating, formatting and parsing datetime values. + @classmethod + def from_string(cls, strval, deftz=None): + try: + ret = dateutil.parser.parse(strval) + except ValueError: + # Sadly, some (but not many) dates which appear in emails can't be + # parsed by dateutil, but can by the email package. I've no idea + # if such dates are rfc compliant, but they do exist in the wild - + # eg: + # "Sat, 11 Oct 2008 13:29:43 -0400 (Eastern Daylight Time)" + try: + utctimestamp = mktime_tz(parsedate_tz(strval)) + ret = datetime.datetime.fromtimestamp(utctimestamp, tzutc()) + except TypeError, exc: + raise ValueError(exc.args[0]) + else: + # dateutil parsed it - now turn it into a UTC value. + # If there is no tzinfo in the string we assume utc. + if ret.tzinfo is None: + if deftz is None: deftz = tzutc() + ret = ret.replace(tzinfo=deftz) + return ret + + @classmethod + def as_string(cls, datetimeval): + return datetimeval.isoformat().split('.')[0].replace('+00:00','Z') + + @classmethod + def as_rfc2822_string(cls, datetimeval): + # Need to pass localtime as that is what the email package expects. + lt = datetimeval.astimezone(tzlocal()) + timestamp = time.mktime(lt.timetuple()) + return email_format_date(timestamp) + + @classmethod + def from_timestamp(cls, tsval, tz=None): + if tz is None: tz = tzutc() + return datetime.datetime.fromtimestamp(tsval, tz) + + @classmethod + def now(cls): + return datetime.datetime.now(tzutc()) diff --git a/linkdrop/public/bg.png b/linkdrop/public/bg.png new file mode 100644 index 0000000000000000000000000000000000000000..69c1798415dadd81f6fac359d0c05e226ca0981f GIT binary patch literal 339 zcmeAS@N?(olHy`uVBq!ia0vp^j0_A+Y8-4p)+FTzdLV(~Aa^H*b?0PW0y%6+-tI08 z{~1m)FszNdXaf}CEbxddW?wc6x=<11Hv2m#DR*|dAc};cpTpw>d4n%z@h!mzD_cK zmTJRW2mNEJyc@Qad=RcNS$DroTlV47wV3M~yB3^ut-U%)RVnBpzxAtxviha52jujd^(J<`;#A5l zaz9q3dh?~>q)&dwny)l(J@xESNyW9?)z53^YE3d+e*fT?Ro4WLp47;-SGriE#@_!# g=p?&ospxX?!b*Q3iz6E+0bS1E>FVdQ&MBb@0B==)#Q*>R literal 0 HcmV?d00001 diff --git a/linkdrop/public/favicon.ico b/linkdrop/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..21e215e337ef0900fd805117ae992f7226269a5c GIT binary patch literal 2862 zcmeHJZ%iCj5Fd7rJy>qv?k)eXcn622KwG5&X>8;!N=l;*Acr~dY_IK=ZsE|Ynv}*r zrGyqF6iBRsB!tsxx8L4}_K(bo>F{c{kDJ&;o@f-~R)wO6}gXkhYC#E39 zPW4{>2>eZiZ#@fKjtfFPl%K*8rxEZ5Y2Ta_V0~fYJ5K!xP!XU#{GC+i3s~o2U=I+F z{wdH}2}gagLld;G9lS3aHAjKv=Yz{{qrNCkeW}gEP@##`&rNVf`k8^2>!Uh3)T;`D%MUh#Yu{z+Wqj_i@6Y#7V2Pc8?=B@y@_0N_(W5a%5hHum2DjT< zDkMKyVz;^7E7Df2h_@-L6L>!HfihdXn|xbJ*5eNs*YY{yQ#N;6_L}%d))u;IYctr| z^$)E{^F8MCIr3^9wGL;x&lekEQ@)W~$MREM^VG7ED!D3Ck~r`C#EjaNW`4o-`MlND z%WiY;!o0WN*~}$&yZ4SxVmjYi^CG+QOrxaFkD8FA*EGiJ729~dd<(C88%%o5u3K>< zkNy=`(z9Ya(j2Cqh7K!g`xP2iZ0um@eaV7ZTYZ?g_Y_+94P(akAXYXqeEtnfB*)0D zZ?~c&PdJ=vH+Ai7vtnI~f(>m9OSdGV<&_Un?EW651D_)OyD;J4HyFR?43=zOhQT%| zl56Z`&lL{M;~Ux(e0oPL#szwjy)}Vs|0gIPyM*%b-;s5kL#gX)6n76}d0jjP+vm6% z__R#Zo4dMI+_q0fb$QCUgs$-$Z0`ul?@UFoa^Gc?2d*M(8AVI?cgR-eeY#hLLrBh4SGaFeb;{Y!;#N z!QzDXMTPf9Z_;e&7>jkI{Ps_%oVj#LIrqkC6o4E)cV$9+O%bwHMm3Oa3`d0@2LZx3v zsqHi5thR>5unRHXR3wyaIwkHn`m4C}_#fu=^=GJV=|cSf8LPQuM~F*x?grjuJRz5) c{MUEJAU$9nl72KBFFNB*H{+Of-+Ise4S{?VMgRZ+ literal 0 HcmV?d00001 diff --git a/linkdrop/public/index.html b/linkdrop/public/index.html new file mode 100644 index 0000000..0719061 --- /dev/null +++ b/linkdrop/public/index.html @@ -0,0 +1,137 @@ + + + + + Welcome to Pylons! + + + +
+

Welcome to Logo displaying the word Pylons +

+
+
+

Let's begin!

+

If you haven't used Pylons before, start with the beginners' tutorial.

+
+ + +
+ + diff --git a/linkdrop/public/pylons-logo.gif b/linkdrop/public/pylons-logo.gif new file mode 100644 index 0000000000000000000000000000000000000000..61b2d9ab369e8d161b5c2fd9e484be28d11b2486 GIT binary patch literal 2399 zcmV-l383~zNk%w1VLJgt0M!Ekii(Q!^YeOnbtx$+T3TAy*Vi2#9ipP5=;-K3NJtbD z6eT4k#>U3-%rYS%A)k<085tS2wzh6zD6Nz%JUlzzzB!G1F2bonA0QtxGBGG9Crm^p z8yp)P8yp)N8XFuO92^`P8X6-bA|4(dA0HnlCnqK*CL$stARr(hA0Qte9w;a%BO@au zBqb*(C?q2zBqSs#Cnpsa78n^B7Z(>77#9~878n>95D*X&5)vvZDjORcD=RB+Zf^h9 zF#rGmA^8LW00093EC2ui06PIh000L6z=3c`EE78iP# zn0OZ!j&TG(prN9pq@V}~J%5r;2?PQHiUg~(kan53m<BnYCEvX<`=hwL5I_sL=bWM4J%JH}Gro)>@?PEuf77zjY_qfas{D|ITB zq1augaDFaF?Fo&({hjgvz72@C>7gsDUZWR^*p5^5?s z3Xw#->F5thI-yma3^mY2oDG?a}kf@GE2>J`5O7z%Tq2VN9tgFI6Iz*$;N*iLCXHI+Twz3TY<*pn3 zltC-C5`g1v?hw-d;5(HDVww_glF}}Z%^qJRIoD5HoMC&YZ41_@+dkt9gM^pi|n*}M)VLbcy15MO9E1xSwxU#b{YT!Esjm| z17I)oK-V=tfwN2`-dl<{fQ!;Lv-KEJxhNwgkwMY)8Syb+MkxEY)ru*`xUF<1KFtNI zcl+?!pafn2#N7%4+jl7E3Y~coyXV`)=Apbd`Y19+ zf>l)mMv7#pLMAwoIC(3D?TDAdKq<;!KV;lFJXnDZHco_bDo!k{fWR)1>R3xKVDQq% z1PW3AO-nPm;r6m9q=A{S2LkY51N3LcBEBd92}ooDis*zl{=!71iX-?2pp6LDF&KAr zq`dU_pCk0hY4!*s<2vX%5Ehbu%M-&DDJ8=$dUBH5pyLvVr$SARVv9KBq*M^8!9PN; zl)jW76An3s8g4^@E0kgp-WLTIzN!ee6a_B#_)0~=N|dV$Vx3I+M;IP~m}78eKmy=R z_bF3=O#EMd_;^cQlERgd5@$e=gUxIvjGNs=WiB65rtA^HfUA_H2}KpDJRX6I!;G3x zs8UaRVhW%6L?boDAkPCe!GNEHB5(j0of=NFlMho307X%shOUm3)M)@jBkCommBE7l z0kPyjS=cHOWpF_pvD7##X>YYwvvqb1pM(t*e zrG0II$RfDw4X_#8o60tb=Uua%Byb=bk}4K7zt{EK zKcV#?01NoSjB71#cRL0I##g@Q>=1+B(YYc)_@JFVqS5waVGO&tU>dG%f!pw1t0qgq z(5&w^?kl$jMwo&_7_mE0(&85z*aEFg}*DP+z`w#PvGG5bo_-2a%*TyT!FoaapEI@|fqc+RuAy6jk(SX2tMO@eyR z@?3UGQ_aFi9#A9lS;N-J^-1Q6D-j04pUabpmQfDWR3;W>o6vPVCU zPW3mp(P=r1%N3ewvb_=+1qxXD)|2jm(qNNBRr|UK-{tYIBeBR~8~ee*IPwzS<~%tBB5+Sty*v$M_ZZgatyKk~M?$L%v_lKb4~HgWUP&2DzDlGN>vceagb RV|m*<+V7V2y~z*+06TypDp&vj literal 0 HcmV?d00001 diff --git a/linkdrop/simple_oauth.py b/linkdrop/simple_oauth.py new file mode 100644 index 0000000..c468f52 --- /dev/null +++ b/linkdrop/simple_oauth.py @@ -0,0 +1,401 @@ +# Copyright 2010 Google Inc. +# Portions copyright Mozilla Messaging 2010. +# +# 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. + +# Code lifted from google's XOAUTH sample; almost identical to that sample +# but with print statements removed or replaced with logging. +# Also added some raindrop-specific helper functions... + +"""Utilities for XOAUTH authentication. + +This script has the following modes of operation: + --generate_oauth_token + --generate_xoauth_string + --test_imap_authentication + --test_smtp_authentication + +The --generate_oauth_token mode will generate and authorize an OAuth token for +testing. + + xoauth --generate_oauth_token --user=xxx@googlemail.com + +The script will converse with Google Accounts and generate an oauth request +token, then present you with a URL you should visit in your browser to authorize +the token. Once you get the verification code from the website, enter it into +the script to get your OAuth access token. The output from this command will be +two values: an OAuth token and an OAuth token secret. These values are reusable, +so if you save them somewhere you won't have to keep repeating this first step. + +The --generate_xoauth_string option generates an XOauth auth string that can +be fed directly to IMAP or SMTP. + +(3-legged OAuth) + xoauth --generate_xoauth_string --user=xxx@googlemail.com + --oauth_token=k99hfs9dh --oauth_token_secret=sd9fhidfskfj + +(2-legged OAuth) + xoauth --generate_xoauth_string --user=xxx@googlemail.com + --consumer_key=foo.com --consumer_secret=sd9fhidfskfj + --xoauth_requestor_id=xxx@googlemail.com + +The output of this mode will be a base64-encoded string. To use it, connect to +imap.googlemail.com:993 and pass it as the second argument to the AUTHENTICATE +command. + + a AUTHENTICATE XOAUTH a9sha9sfs[...]9dfja929dk== + +The --test_imap_authentication and --test_smtp_authentication comands generate +an XOAUTH string and use them to authenticate to a live IMAP or SMTP server. +You can use the --imap_hostname and --smtp_hostname options to specify the +server to connect to. + + xoauth --test_imap_authentication --user=xxx@googlemail.com + --oauth_token=k99hfs9dh --oauth_token_secret=sd9fhidfskfj + + xoauth --test_smtp_authentication --user=xxx@googlemail.com + --oauth_token=k99hfs9dh --oauth_token_secret=sd9fhidfskfj + +""" + +import base64 +import hmac +import imaplib +import random +import hashlib +import smtplib +import sys +import time +import urllib +import logging + +logger = logging.getLogger(__name__) + + +def UrlEscape(text): + # See OAUTH 5.1 for a definition of which characters need to be escaped. + return urllib.quote(text, safe='~-._') + + +def UrlUnescape(text): + # See OAUTH 5.1 for a definition of which characters need to be escaped. + return urllib.unquote(text) + + +def FormatUrlParams(params): + """Formats parameters into a URL query string. + + Args: + params: A key-value map. + + Returns: + A URL query string version of the given parameters. + """ + param_fragments = [] + for param in sorted(params.iteritems(), key=lambda x: x[0]): + param_fragments.append('%s=%s' % (param[0], UrlEscape(param[1]))) + return '&'.join(param_fragments) + + +def EscapeAndJoin(elems): + return '&'.join([UrlEscape(x) for x in elems]) + + +def GenerateSignatureBaseString(method, request_url_base, params): + """Generates an OAuth signature base string. + + Args: + method: The HTTP request method, e.g. "GET". + request_url_base: The base of the requested URL. For example, if the + requested URL is + "https://mail.google.com/mail/b/xxx@googlemail.com/imap/?" + + "xoauth_requestor_id=xxx@googlemail.com", the request_url_base would be + "https://mail.google.com/mail/b/xxx@googlemail.com/imap/". + params: Key-value map of OAuth parameters, plus any parameters from the + request URL. + + Returns: + A signature base string prepared according to the OAuth Spec. + """ + return EscapeAndJoin([method, request_url_base, FormatUrlParams(params)]) + + +def GenerateHmacSha1Signature(text, key): + digest = hmac.new(key, text, hashlib.sha1) + return base64.b64encode(digest.digest()) + + +def GenerateOauthSignature(base_string, consumer_secret, token_secret): + key = EscapeAndJoin([consumer_secret, token_secret]) + return GenerateHmacSha1Signature(base_string, key) + + +def ParseUrlParamString(param_string): + """Parses a URL parameter string into a key-value map. + + Args: + param_string: A URL parameter string, e.g. "foo=bar&oof=baz". + + Returns: + A key-value dict. + """ + kv_pairs = param_string.split('&') + params = {} + for kv in kv_pairs: + k, v = kv.split('=') + params[k] = UrlUnescape(v) + return params + + +class OAuthEntity(object): + """Represents consumers and tokens in OAuth.""" + + def __init__(self, key, secret, **rest): + self.key = key + self.secret = secret + for n, v in rest.iteritems(): + setattr(self, n, v) + + def __repr__(self): + return "" % self.__dict__ + + +def FillInCommonOauthParams(params, consumer, nonce=None, timestamp=None): + """Fills in parameters that are common to all oauth requests. + + Args: + params: Parameter map, which will be added to. + consumer: An OAuthEntity representing the OAuth consumer. + nonce: optional supplied nonce + timestamp: optional supplied timestamp + """ + params['oauth_consumer_key'] = consumer.key + if nonce: + params['oauth_nonce'] = nonce + else: + params['oauth_nonce'] = str(random.randrange(2**64 - 1)) + params['oauth_signature_method'] = 'HMAC-SHA1' + params['oauth_version'] = '1.0' + if timestamp: + params['oauth_timestamp'] = timestamp + else: + params['oauth_timestamp'] = str(int(time.time())) + + +def GenerateRequestToken(consumer, scope, nonce, timestamp, callback_url, + google_accounts_url_generator): + """Generates an OAuth request token by talking to Google Accounts. + + Args: + consumer: An OAuthEntity representing the OAuth consumer. + scope: Scope for the OAuth access token. + nonce: The nonce to use in the signature. If None is passed, a random nonce + will be generated. + timestamp: Timestamp to use in the signature. If None is passed, the current + time will be used. + google_accounts_url_generator: function that creates a Google Accounts URL + for the given URL fragment. + + Returns: + An OAuthEntity representing the request token. + """ + params = {} + FillInCommonOauthParams(params, consumer, nonce, timestamp) + #params['oauth_callback'] = 'oob' + params['oauth_callback'] = callback_url + params['scope'] = scope + request_url = google_accounts_url_generator.GetRequestTokenUrl() + token = OAuthEntity(None, '') + base_string = GenerateSignatureBaseString('GET', request_url, params) + signature = GenerateOauthSignature(base_string, consumer.secret, + token.secret) + params['oauth_signature'] = signature + + url = '%s?%s' % (request_url, FormatUrlParams(params)) + response = urllib.urlopen(url).read() + response_params = ParseUrlParamString(response) + + # for param in response_params.items(): + # print '%s: %s' % param + + key = response_params.pop('oauth_token') + secret = response_params.pop('oauth_token_secret') + token = OAuthEntity(key, secret, **response_params) + + #print ('To authorize token, visit this url and follow the directions ' + # 'to generate a verification code:') + + #print ' %s?oauth_token=%s' % ( + # google_accounts_url_generator.GetAuthorizeTokenUrl(), + # UrlEscape(response_params['oauth_token'])) + return token + + +def GetAccessToken(consumer, request_token, oauth_verifier, + google_accounts_url_generator): + """Obtains an OAuth access token from Google Accounts. + + Args: + consumer: An OAuth entity representing the OAuth consumer. + request_token: An OAuthEntity representing the request token (e.g. as + returned by GenerateRequestToken. + oauth_verifier: The verification string displayed to the user after + completing Google Accounts authorization. + google_accounts_url_generator: function that creates a Google Accounts URL + for the given URL fragment. + + Returns: + An OAuthEntity representing the OAuth access token. + """ + params = {} + FillInCommonOauthParams(params, consumer) + params['oauth_token'] = request_token.key + params['oauth_verifier'] = oauth_verifier + request_url = google_accounts_url_generator.GetAccessTokenUrl() + base_string = GenerateSignatureBaseString('GET', request_url, params) + signature = GenerateOauthSignature(base_string, consumer.secret, + request_token.secret) + params['oauth_signature'] = signature + + url = '%s?%s' % (request_url, FormatUrlParams(params)) + response = urllib.urlopen(url).read() + response_params = ParseUrlParamString(response) + #for param in ('oauth_token', 'oauth_token_secret'): + # print '%s: %s' % (param, response_params[param]) + key = response_params.pop('oauth_token') + secret = response_params.pop('oauth_token_secret') + return OAuthEntity(key, secret, **response_params) + + +def GenerateXOauthString(consumer, access_token, user, proto, + xoauth_requestor_id, nonce, timestamp): + """Generates an IMAP XOAUTH authentication string. + + Args: + consumer: An OAuthEntity representing the consumer. + access_token: An OAuthEntity representing the access token. + user: The Google Mail username (full email address) + proto: "imap" or "smtp", for example. + xoauth_requestor_id: xoauth_requestor_id URL parameter for 2-legged OAuth + nonce: optional supplied nonce + timestamp: optional supplied timestamp + + Returns: + A string that can be passed as the argument to an IMAP + "AUTHENTICATE XOAUTH" command after being base64-encoded. + """ + method = 'GET' + url_params = {} + if xoauth_requestor_id: + url_params['xoauth_requestor_id'] = xoauth_requestor_id + oauth_params = {} + FillInCommonOauthParams(oauth_params, consumer, nonce, timestamp) + if access_token.key: + oauth_params['oauth_token'] = access_token.key + signed_params = oauth_params.copy() + signed_params.update(url_params) + request_url_base = ( + 'https://mail.google.com/mail/b/%s/%s/' % (user, proto)) + base_string = GenerateSignatureBaseString( + method, + request_url_base, + signed_params) + logger.debug('signature base string: %s', base_string) + signature = GenerateOauthSignature(base_string, consumer.secret, + access_token.secret) + oauth_params['oauth_signature'] = signature + + formatted_params = [] + for k, v in sorted(oauth_params.iteritems()): + formatted_params.append('%s="%s"' % (k, UrlEscape(v))) + param_list = ','.join(formatted_params) + if url_params: + request_url = '%s?%s' % (request_url_base, + FormatUrlParams(url_params)) + else: + request_url = request_url_base + preencoded = '%s %s %s' % (method, request_url, param_list) + logger.debug('xoauth string: %s' + preencoded) + return preencoded + +class GoogleAccountsUrlGenerator: + def __init__(self, user): + self.__apps_domain = None + at_index = user.find('@') + if at_index != -1 and (at_index + 1) < len(user): + domain = user[(at_index + 1):].lower() + if domain != 'gmail.com' and domain != 'googlemail.com': + self.__apps_domain = domain + + def GetRequestTokenUrl(self): + return 'https://www.google.com/accounts/OAuthGetRequestToken' + + def GetAuthorizeTokenUrl(self): + if self.__apps_domain: + return ('https://www.google.com/a/%s/OAuthAuthorizeToken' % + self.__apps_domain) + else: + return 'https://www.google.com/accounts/OAuthAuthorizeToken' + + def GetAccessTokenUrl(self): + return 'https://www.google.com/accounts/OAuthGetAccessToken' + +# Some raindrop specific stuff... + +class TwitterAccountsUrlGenerator: + + def GetRequestTokenUrl(self): + return 'http://twitter.com/oauth/request_token' + + def GetAuthorizeTokenUrl(self): + return 'http://twitter.com/oauth/authorize' + + def GetAccessTokenUrl(self): + return 'http://twitter.com/oauth/access_token' + +def getOAuthUrlGenerator(provider, user): + if provider == 'twitter.com': + return TwitterAccountsUrlGenerator() + elif provider in ['gmail.com', 'google.com']: + return GoogleAccountsUrlGenerator(user) + else: + raise Exception('OAuth provider %s not supported' % provider) + +def GenerateXOauthStringFromAcctInfo(protocol, acct_info): + """Generates an IMAP XOAUTH authentication string from a raindrop + 'account info' dictionary. + """ + username = acct_info.username + oauth_token = getattr(acct_info, 'oauth_token', None) + oauth_token_secret = getattr(acct_info, 'oauth_token_secret', None) + consumer_key = getattr(acct_info, 'oauth_consumer_key', 'anonymous') + consumer_secret = getattr(acct_info, 'oauth_consumer_secret', 'anonymous') + consumer = OAuthEntity(consumer_key, consumer_secret) + google_accounts_url_generator = GoogleAccountsUrlGenerator(username) + access_token = OAuthEntity(oauth_token, oauth_token_secret) + xoauth_requestor_id = None + # the utility functions above will generate nonce and timestamps for us + nonce = None + timestamp = None + xoauth_string = GenerateXOauthString( + consumer, access_token, username, protocol, + xoauth_requestor_id, nonce, timestamp) + return xoauth_string + + +def AcctInfoSupportsOAuth(acct_info): + # A reflection on the need to have a URL generator per provider is that + # we only support gmail for now... + is_google = getattr(acct_info, 'kind', None) == 'gmail' or \ + getattr(acct_info, 'host', '').endswith('gmail.com') + return is_google and getattr(acct_info, 'oauth_token', None) and getattr(acct_info, 'oauth_token_secret', None) diff --git a/linkdrop/tests/__init__.py b/linkdrop/tests/__init__.py new file mode 100644 index 0000000..456f680 --- /dev/null +++ b/linkdrop/tests/__init__.py @@ -0,0 +1,34 @@ +"""Pylons application test package + +This package assumes the Pylons environment is already loaded, such as +when this script is imported from the `nosetests --with-pylons=test.ini` +command. + +This module initializes the application via ``websetup`` (`paster +setup-app`) and provides the base testing objects. +""" +from unittest import TestCase + +from paste.deploy import loadapp +from paste.script.appinstall import SetupCommand +from pylons import url +from routes.util import URLGenerator +from webtest import TestApp + +import pylons.test + +__all__ = ['environ', 'url', 'TestController'] + +# Invoke websetup with the current config file +SetupCommand('setup-app').run([pylons.test.pylonsapp.config['__file__']]) + +environ = {} + +class TestController(TestCase): + + def __init__(self, *args, **kwargs): + wsgiapp = pylons.test.pylonsapp + config = wsgiapp.config + self.app = TestApp(wsgiapp) + url._push_object(URLGenerator(config['routes.map'], environ)) + TestCase.__init__(self, *args, **kwargs) diff --git a/linkdrop/tests/functional/__init__.py b/linkdrop/tests/functional/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/linkdrop/tests/test_models.py b/linkdrop/tests/test_models.py new file mode 100644 index 0000000..e69de29 diff --git a/linkdrop/websetup.py b/linkdrop/websetup.py new file mode 100644 index 0000000..9a42df6 --- /dev/null +++ b/linkdrop/websetup.py @@ -0,0 +1,18 @@ +"""Setup the linkdrop application""" +import logging + +import pylons.test + +from linkdrop.config.environment import load_environment +from linkdrop.model.meta import Session, Base + +log = logging.getLogger(__name__) + +def setup_app(command, conf, vars): + """Place any commands to setup linkdrop here""" + # Don't reload the app if it was loaded under the testing environment + if not pylons.test.pylonsapp: + load_environment(conf.global_conf, conf.local_conf) + + # Create the tables if they don't already exist + Base.metadata.create_all(bind=Session.bind) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..eec681b --- /dev/null +++ b/setup.cfg @@ -0,0 +1,31 @@ +[egg_info] +tag_build = dev +tag_svn_revision = true + +[easy_install] +find_links = http://www.pylonshq.com/download/ + +[nosetests] +with-pylons = test.ini + +# Babel configuration +[compile_catalog] +domain = linkdrop +directory = linkdrop/i18n +statistics = true + +[extract_messages] +add_comments = TRANSLATORS: +output_file = linkdrop/i18n/linkdrop.pot +width = 80 + +[init_catalog] +domain = linkdrop +input_file = linkdrop/i18n/linkdrop.pot +output_dir = linkdrop/i18n + +[update_catalog] +domain = linkdrop +input_file = linkdrop/i18n/linkdrop.pot +output_dir = linkdrop/i18n +previous = true diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ef7d0f6 --- /dev/null +++ b/setup.py @@ -0,0 +1,37 @@ +try: + from setuptools import setup, find_packages +except ImportError: + from ez_setup import use_setuptools + use_setuptools() + from setuptools import setup, find_packages + +setup( + name='linkdrop', + version='0.1', + description='', + author='', + author_email='', + url='', + install_requires=[ + "Pylons>=1.0", + "SQLAlchemy>=0.5", + ], + setup_requires=["PasteScript>=1.6.3"], + packages=find_packages(exclude=['ez_setup']), + include_package_data=True, + test_suite='nose.collector', + package_data={'linkdrop': ['i18n/*/LC_MESSAGES/*.mo']}, + #message_extractors={'linkdrop': [ + # ('**.py', 'python', None), + # ('templates/**.mako', 'mako', {'input_encoding': 'utf-8'}), + # ('public/**', 'ignore', None)]}, + zip_safe=False, + paster_plugins=['PasteScript', 'Pylons'], + entry_points=""" + [paste.app_factory] + main = linkdrop.config.middleware:make_app + + [paste.app_install] + main = pylons.util:PylonsInstaller + """, +) diff --git a/test.ini b/test.ini new file mode 100644 index 0000000..261625d --- /dev/null +++ b/test.ini @@ -0,0 +1,21 @@ +# +# linkdrop - Pylons testing environment configuration +# +# The %(here)s variable will be replaced with the parent directory of this file +# +[DEFAULT] +debug = true +# Uncomment and replace with the address which should receive any error reports +#email_to = you@yourdomain.com +smtp_server = localhost +error_email_from = paste@localhost + +[server:main] +use = egg:Paste#http +host = 127.0.0.1 +port = 5000 + +[app:main] +use = config:development.ini + +# Add additional test specific configuration options as necessary. diff --git a/web/scratch/README.txt b/web/scratch/README.txt new file mode 100644 index 0000000..d88f4a7 --- /dev/null +++ b/web/scratch/README.txt @@ -0,0 +1,6 @@ +This is a "scratch pad" folder - it is used for items to aid with development +before the "real" application has matured. As a result, they tend to be very +light in terms of styling and design elements - they are just bare-bones. + +Items here should be short-lived and removed from here once they are replaced +with real UI. diff --git a/web/scratch/oauth/index.html b/web/scratch/oauth/index.html new file mode 100644 index 0000000..b18ed89 --- /dev/null +++ b/web/scratch/oauth/index.html @@ -0,0 +1,168 @@ + + + + + + Links keep dropping on my head + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/scratch/oauth/index.js b/web/scratch/oauth/index.js new file mode 100644 index 0000000..f8395d3 --- /dev/null +++ b/web/scratch/oauth/index.js @@ -0,0 +1,121 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (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.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Raindrop. + * + * The Initial Developer of the Original Code is + * Mozilla Messaging, Inc.. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * */ + +/*jslint plusplus: false */ +/*global require: false, location: true, window: false, alert: false */ +"use strict"; + +require.def("signup", + ["require", "jquery", "blade/fn", "rdapi", "placeholder", "blade/url"], +function (require, $, fn, rdapi, placeholder, url) { + + //Given a new rd-token, save it to local storage + var search = window.location.href.split('?')[1]; + if (search) { + search = search.split('#')[0]; + var args = url.queryToObject(search); + if (args['rd-token']) { + localStorage['X-Raindrop-Token'] = args['rd-token']; + } + } + var rdtoken = localStorage['X-Raindrop-Token']; + if (rdtoken) { + $('form').each(function(i, node) { + node = $(node); + node.append('') + }) + } + + var validHashRegExp = /^\w+$/; + + function onHashChange() { + var value = location.hash.split("#")[1], + start, end; + + value = value || "welcome"; + + if (validHashRegExp.test(value)) { + $(".section").each(function (i, node) { + node = $(node); + if (node.hasClass(value)) { + end = node; + } else if (!node.hasClass("hidden")) { + start = node; + } + }); + } + + //Animate! + if (start) { + //Start node + start.fadeOut(600, function () { + start.addClass("hidden"); + }); + } + + if (end) { + //End node + end.removeClass("hidden") + .fadeIn(600); + } + } + + //Set up hashchange listener + window.addEventListener("hashchange", onHashChange, false); + + $(function () { + + $("#oauthForm") + .submit(function (evt) { + //First clear old errors + $(".error").addClass("invisible"); + + var form = evt.target, + isError = false; + + //Make sure all form elements are trimmed and username exists. + $.each(form.elements, function (i, node) { + var trimmed = node.value.trim(); + + if (node.getAttribute("placeholder") === trimmed) { + trimmed = ""; + } + + node.value = trimmed; + }); + + if (isError) { + $(".usernameError", form).removeClass("invisible"); + placeholder(form); + evt.stopPropagation(); + evt.preventDefault(); + } + }) + .each(function (i, node) { + placeholder(node); + }); + + //Make sure we set up initial state + onHashChange(); + }); +}); diff --git a/web/scripts/blade/defer.js b/web/scripts/blade/defer.js new file mode 100644 index 0000000..4f45787 --- /dev/null +++ b/web/scripts/blade/defer.js @@ -0,0 +1,99 @@ +/** + * @license blade/defer Copyright (c) 2010, The Dojo Foundation All Rights Reserved. + * Available via the MIT, GPL or new BSD license. + * see: http://github.com/jrburke/blade for details + */ +/*jslint nomen: false, plusplus: false */ +/*global require: false */ + +'use strict'; + +require.def('blade/defer', ['blade/fn', 'blade/dispatch'], function (fn, bladeDispatch) { + + /** + * Creates an object representing a deferred action. + * @param {Function} [onCancel] optional function to call if the deferred + * action is canceled + * @param {Array} otherEventNames an array of event names to also allow + * sending and notifying on this type of deferred action. This allows you + * to express more complex interactions besides something that just indicates + * "ok", "error" or "cancel". + * @returns {Object} object representing the deferred action. It contains + * two properties: + * send: a function to send events. It takes a string name for the event, + * "ok", "error" or "cancel", and a value. + * listener: an object that only exposes an "ok", "error" and "cancel" + * functions that allow listening to those respective events. If otherEventNames + * specified other events, then there are listener registration functions + * for those event names too. + */ + function defer(onCancel, otherEventNames) { + var dfd = {}, + sentName, i, evtName, + dispatch = bladeDispatch.make(), + makeCb = function (name) { + return function (obj, f) { + var cb = fn.bind(obj, f); + dispatch.onAfter(name, function (evt) { + return cb(evt.returnValue); + }, true); + return dfd.listener; + }; + }; + + //Set up the cancellation action if desired. + if (onCancel) { + dispatch.onAfter('cancel', function (evt) { + return onCancel(); + }); + } + + dfd.send = function (name, value) { + //Do not allow sending more than one message for the deferred. + if (sentName) { + throw new Error('blade/defer object already sent event: ' + sentName); + } + sentName = name; + + dispatch.send({ + name: name, + args: [value], + persist: true + }); + + //If no error handlers on this deferred, be sure to at least + //log it to allow some sort of debugging. + if (name === 'error' && + (!dispatch._dispatchAfterQ || ! dispatch._dispatchAfterQ.error) && + defer.onErrorDefault) { + defer.onErrorDefault(value); + } + + return dfd; + }; + + dfd.listener = { + ok: makeCb('ok'), + error: makeCb('error'), + cancel: makeCb('cancel') + }; + + //Allow wiring up other event names + if (otherEventNames) { + for (var i = 0; (evtName = otherEventNames[i]); i++) { + dfd.listener[name] = makeCb[name]; + } + } + + return dfd; + } + + defer.onErrorDefault = function (err) { + if (typeof console !== 'undefined') { + console.error(err); + } + } + + return defer; +}); + diff --git a/web/scripts/blade/dispatch.js b/web/scripts/blade/dispatch.js new file mode 100644 index 0000000..e70da98 --- /dev/null +++ b/web/scripts/blade/dispatch.js @@ -0,0 +1,227 @@ +/** + * @license blade/dispatch Copyright (c) 2010, The Dojo Foundation All Rights Reserved. + * Available via the MIT, GPL or new BSD license. + * see: http://github.com/jrburke/blade for details + */ +/*jslint nomen: false, plusplus: false */ +/*global require: false */ + +'use strict'; + +require.def('blade/dispatch', ['blade/object', 'blade/fn'], function (object, fn) { + var emptyFunc = function () {}, + mainDispatch, + slice = Array.prototype.slice, + + needBind = function (f) { + return f !== undefined && (typeof f === 'string' || fn.is(f)); + }, + + register = function (type) { + return function (name, obj, f) { + //Adjust args to allow for a bind call + if (needBind(f)) { + f = fn.bind(obj, f); + } else { + f = obj; + } + + var qName = type, + typeQ = this[qName] || (this[qName] = {}), + q = typeQ[name] || (typeQ[name] = []), index; + + index = q.push(f) - 1; + q.count = q.count ? q.count + 1 : 1; + + //Return an unregister function to allow removing + //a listener. Notice that it can make the q array sparsely + //populated. This should be a sparsely populated array + //to allow a callback to unregister itself without affecting + //other callbacks in the array. + return function () { + q[index] = null; + q.count -= 1; + if (q.count === 0) { + delete typeQ[name]; + } + + //Clean up closure references for good measure/avoid leaks. + qName = typeQ = q = null; + }; + }; + }, + + onAfter = register('_dispatchAfterQ'), + + /** + * Defines the dispatch object. You can call its methods for a general + * publish/subscribe mechanism, or mixin its prototype properties + * to another object to give that object dispatch capabilities. + */ + dispatch = { + on: register('_dispatchBeforeQ'), + onAfter: function (name, obj, f, wantValue) { + var doBind = needBind(f), result, value, callback, evt; + //Adjust args if needing a bind + if (doBind) { + callback = f = fn.bind(obj, f); + } else { + wantValue = f; + callback = obj; + } + + result = doBind ? onAfter.call(this, name, f, wantValue) : onAfter.call(this, name, obj, f); + if (wantValue) { + //value is the property on the object, unless it is something + //that should be immutable or does not exist, then only get a value from _dispatchPersisted + value = name in this ? this[name] : + (this._dispatchPersisted && name in this._dispatchPersisted ? this._dispatchPersisted[name] : undefined); + evt = { + preventDefault: emptyFunc, + stopPropagation: emptyFunc, + returnValue: value + }; + + if (value !== undefined) { + callback(evt); + } + } + return result; + }, + + /** + * Sends an event. An event can have its values modified by "before" + * listeners before the default action happens. A "before" listener + * can also prevent the default action from occurring. "after" listeners + * only get to be notified of the return value from the event. + * + * @param {Object||String} message the message can either be an object + * with the following properties: + * @param {String} message.name the name of the message + * @param {Array} message.args the array of arguments for the message + * @param {Boolean}message.persist the result of the send should be + * remembered, so that any subsequent listeners that listen after + * the result is rememberd can opt to get the last good value. + * @param {Function} [message.defaultAction] a default action to take + * if any of the "before" listeners do not call preventDefault() + * on the event object they receive. + * + * If message is a string, then that is like the "name" property mentioned + * above, and any additional function arguments are treated as the + * args array. + * + * If defaultAction is not passed, then the default action will be to + * either set the property value on this object that matches the name + * to the first arg value, or if the name maps to function property + * on the object, it will call that function with the args. + * + * @returns {Object} the returnValue from any + */ + send: function (message) { + if (typeof message === 'string') { + //Normalize message to object arg form. + message = { + name: message, + args: slice.call(arguments, 1) + }; + } + + var name = message.name, + beforeQ = this._dispatchBeforeQ && this._dispatchBeforeQ[name], + afterQ = this._dispatchAfterQ && this._dispatchAfterQ[name], + preventDefault = false, stopImmediatePropagation, + evt = { + preventDefault: function () { + preventDefault = true; + }, + stopImmediatePropagation: function () { + stopImmediatePropagation = true; + }, + args: message.args + }, + i, result, value, args, isFunc, persisted; + + //Trigger before listeners + if (beforeQ) { + for (i = 0; i < beforeQ.length; i++) { + //array can be sparse because of unregister functions + if (beforeQ[i]) { + beforeQ[i](evt); + if (stopImmediatePropagation) { + break; + } + } + } + } + + //If a before handler prevents the default action, exit + //early, using any return value found in the event that may + //have been set by a before handler. + if (preventDefault) { + return evt.returnValue; + } + + //Do the default action. + if (message.defaultAction) { + result = message.defaultAction.apply(this, evt.args); + } else { + //Only bother if the property already exists on the object, + //otherwise it is a catch all or just an event router + args = evt.args; + if (name in this) { + isFunc = fn.is(this[name]); + value = this[name]; + + if (args && args.length) { + //A set operation + result = isFunc ? value.apply(this, args) : this[name] = args[0]; + } else { + //A get operation + result = isFunc ? this[name]() : value; + } + } else if (this._dispatchCatchAll) { + //Allow the catch all to get it. + result = this._dispatchCatchAll(name, args); + } else { + result = args && args[0]; + } + } + + //Trigger mutable after listeners first, before the immutable ones + //to allow the mutable ones to modify the result. + if (afterQ) { + stopImmediatePropagation = false; + evt.returnValue = result; + for (i = 0; i < afterQ.length; i++) { + //array can be sparse because of unregister functions + if (afterQ[i]) { + afterQ[i](evt); + if (stopImmediatePropagation) { + break; + } + } + } + result = evt.returnValue; + } + + //Hold on to the result if need be. Useful for the deferred/promise + //cases where listeners can be added after the deferred completes. + if (message.persist) { + persisted = this._dispatchPersisted || (this._dispatchPersisted = {}); + persisted[message.name] = result; + } + + return result; + } + }; + + //Create a top level dispatch that can be used for "global" event routing, + //and which can make new dispatch objects that have all the methods above, + //but without the instance variables. + mainDispatch = object.create(dispatch); + mainDispatch.make = function () { + return object.create(dispatch); + }; + + return mainDispatch; +}); diff --git a/web/scripts/blade/fn.js b/web/scripts/blade/fn.js new file mode 100644 index 0000000..785a865 --- /dev/null +++ b/web/scripts/blade/fn.js @@ -0,0 +1,58 @@ +/** + * @license blade/func Copyright (c) 2010, The Dojo Foundation All Rights Reserved. + * Available via the MIT, GPL or new BSD license. + * see: http://github.com/jrburke/blade for details + */ +/*jslint nomen: false, plusplus: false */ +/*global require: false */ + +'use strict'; + +require.def('blade/fn', function () { + var slice = Array.prototype.slice, + ostring = Object.prototype.toString, + + fn = { + /** + * Determines if the input a function. + * @param {Object} it whatever you want to test to see if it is a function. + * @returns Boolean + */ + is: function (it) { + return ostring.call(it) === '[object Function]'; + }, + + /** + * Different from Function.prototype.bind in ES5 -- + * it has the "this" argument listed first. This is generally + * more readable, since the "this" object is visible before + * the function body, reducing chances for error by missing it. + * If only obj has a real value then obj will be returned, + * allowing this method to be called even if you are not aware + * of the format of the obj and f types. + * It also allows the function to be a string name, in which case, + * obj[f] is used to find the function. + * @param {Object||Function} obj the "this" object, or a function. + * @param {Function||String} f the function of function name that + * should be called with obj set as the "this" value. + * @returns {Function} + */ + bind: function (obj, f) { + //Do not bother if + if (!f) { + return obj; + } + + //Make sure we have a function + if (typeof f === 'string') { + f = obj[f]; + } + var args = slice.call(arguments, 2); + return function () { + return f.apply(obj, args.concat(slice.call(arguments, 0))); + }; + } + }; + + return fn; +}); diff --git a/web/scripts/blade/jig.js b/web/scripts/blade/jig.js new file mode 100644 index 0000000..6b32073 --- /dev/null +++ b/web/scripts/blade/jig.js @@ -0,0 +1,863 @@ +/** + * @license blade/jig Copyright (c) 2010, The Dojo Foundation All Rights Reserved. + * Available via the MIT, GPL or new BSD license. + * see: http://github.com/jrburke/blade for details + */ +/*jslint nomen: false, plusplus: false */ +/*global require: false, document: false, console: false, jQuery: false */ + +'use strict'; + +require.def('blade/jig', + ['require', 'blade/object'], +function (require, object) { + + //Fix unit test: something is wrong with it, says it passes, but + //with attachData change, the string is actually different now. + //TODO: for attachData, only generate a new ID when the data value changes, + //and similarly, only attach the data one time per data value. + + //If have browser tries to fetch + //{foo} if that is in markup. Doing a <{/}img, then FF browser treats that + //as <{/}img. Using b; + }, + gte: function (a, b) { + return a >= b; + }, + lt: function (a, b) { + return a < b; + }, + lte: function (a, b) { + return a <= b; + }, + or: function (a, b) { + return !!(a || b); + }, + and: function (a, b) { + return !!(a && b); + }, + is: function (a) { + return !!a; + }, + eachProp: function (obj) { + //Converts object properties into an array + //of objects that have 'prop' and 'value' properties. + var prop, ret = []; + for (prop in obj) { + if (obj.hasOwnProperty(prop)) { + ret.push({ + prop: prop, + value: obj[prop] + }); + } + } + + //Sort the names to be roughly alphabetic + return ret.sort(function (a, b) { + return a.prop > b.prop ? 1 : -1; + }); + } + }, + attachData = false, + dataIdCounter = 1, + controlIdCounter = 1, + dataRegistry = {}, + tempNode = typeof document !== 'undefined' && document.createElement ? + document.createElement('div') : null, + templateClassRegExp = /(\s*)(template)(\s*)/; + + function isArray(it) { + return ostring.call(it) === '[object Array]'; + } + + /** + * Gets a property from a context object. Allows for an alternative topContext + * object that can be used for the first part property lookup if it is not + * found in context first. + * @param {Array} parts the list of nested properties to look up on a context. + * @param {Object} context the context to start the property lookup + * @param {Object} [topContext] an object to use as an alternate context + * for the very first part property to look up if it is not found in context. + * @returns {Object} + */ + function getProp(parts, context, topContext) { + var obj = context, i, p; + for (i = 0; obj && (p = parts[i]); i++) { + obj = (typeof obj === 'object' && p in obj ? obj[p] : (topContext && i === 0 && p in topContext ? topContext[p] : undefined)); + } + return obj; // mixed + } + + function strToInt(value) { + return value ? parseInt(value, 10) : 0; + } + + function getObject(name, data, options) { + var brackRegExp = /\[([\w0-9\.'":]+)\]/, + part = name, + parent = data, + isTop = true, + match, pre, prop, obj, startIndex, endIndex, indices, result, + parenStart, parenEnd, func, funcName, arg, args, i, firstChar; + + //If asking for the default arg it means giving back the current data. + if (name === defaultArg) { + return data; + } + + //If name is just an integer, just return it. + if (wordRegExp.test(name)) { + return strToInt(name); + } + + //An empty string is just returned. + if (name === '') { + return ''; + } + + //If the name looks like a string, just return that. + firstChar = name.charAt(0); + if (firstChar === "'" || firstChar === "'") { + return name.substring(1, name.length - 1); + } + + //First check for function call. Function must be globally visible. + if ((parenStart = name.indexOf('(')) !== -1) { + parenEnd = name.lastIndexOf(')'); + funcName = name.substring(0, parenStart); + func = options.fn[funcName]; + if (!func) { + jig.error('Cannot find function named: ' + funcName + ' for ' + name); + return ''; + } + arg = name.substring(parenStart + 1, parenEnd); + if (arg.indexOf(',') !== -1) { + args = arg.split(','); + for (i = args.length - 1; i >= 0; i--) { + args[i] = getObject(args[i], data, options); + } + result = func.apply(null, args); + } else { + result = func(getObject(arg, data, options)); + } + if (parenEnd < name.length - 1) { + //More data properties after the function call, fetch them + //If the part after the paren is a dot, then skip over that part + if (name.charAt(parenEnd + 1) === '.') { + parenEnd += 1; + } + return getObject(name.substring(parenEnd + 1, name.length), result, options); + } else { + return result; + } + } + + //Now handle regular object references, which could have [] notation. + while ((match = brackRegExp.exec(part))) { + prop = match[1].replace(/['"]/g, ''); + pre = part.substring(0, match.index); + + part = part.substring(match.index + match[0].length, part.length); + if (part.indexOf('.') === 0) { + part = part.substring(1, part.length); + } + + obj = getProp(pre.split('.'), parent, isTop ? options.context : null); + isTop = false; + + if (!obj && prop) { + jig.error('blade/jig: No property "' + prop + '" on ' + obj); + return ''; + } + + if (prop.indexOf(':') !== -1) { + //An array slice action + indices = prop.split(':'); + startIndex = strToInt(indices[0]); + endIndex = strToInt(indices[1]); + + if (!endIndex) { + obj = obj.slice(startIndex); + } else { + obj = obj.slice(startIndex, endIndex); + } + } else { + if (options.strict && !(prop in obj)) { + jig.error('blade/jig: no property "' + prop + '"'); + } + obj = obj[prop]; + } + parent = obj; + } + + if (!part) { + result = parent; + } else { + result = getProp(part.split('.'), parent, isTop ? options.context : null); + } + + if (options.strict && result === undefined) { + jig.error('blade/jig: undefined value for property "' + name + '"'); + } + + return result; + } + + /** + * Gets a compiled template based on the template ID. Will look in the + * DOM for an element with that ID if a template is not found already in + * the compiled cache. + * @param {String} id the ID of the template/DOM node + * @param {Object} [options] + * + * @returns {Array} the compiled template. + */ + function compiledById(id, options) { + options = options || {}; + var compiled = jig.cache(id, options), node; + + //Did not find the text template. Maybe it is a DOM element. + if (compiled === undefined && typeof document !== 'undefined') { + node = document.getElementById(id); + if (node) { + jig.parse([node], options); + } + compiled = jig.cache(id, options); + } + if (compiled === undefined) { + throw new Error('blade/jig: no template or node with ID: ' + id); + } + return compiled; + } + + commands = { + '_default_': { + doc: 'Property reference', + action: function (args, data, options, children, render) { + var value = args[0] ? getObject(args[0], data, options) : data, + comparison = args[1] ? getObject(args[1], data, options) : undefined, + i, text = ''; + + //If comparing to some other value, then the value is the data, + //and need to compute if the values compare. + if (args[1]) { + comparison = value === comparison; + value = data; + } else { + //Just use the value, so the value is used in the comparison. + comparison = value; + } + //Want to allow returning 0 for values, so this next check is + //a bit verbose. + if (comparison === false || comparison === null || + comparison === undefined || (isArray(comparison) && !comparison.length)) { + return ''; + } else if (children) { + if (isArray(value)) { + for (i = 0; i < value.length; i++) { + text += render(children, value[i], options); + } + } else { + //If the value is true or false, then just use parent data. + //for the child rendering. + if (typeof value === 'boolean') { + value = data; + } + text = render(children, value, options); + } + } else { + text = value; + } + return text; + } + }, + '!': { + doc: 'Not', + action: function (args, data, options, children, render) { + var value = getObject(args[0], data, options), + comparison = args[1] ? getObject(args[1], data, options) : undefined; + + //If comparing to some other value, then the value is the data, + //and need to compute if the values compare. + if (args[1]) { + comparison = value === comparison; + value = data; + } else { + //Just use the value, so the value is used in the comparison. + comparison = value; + } + + if (children && !comparison) { + return render(children, data, options); + } + return ''; + } + }, + '#': { + doc: 'Template reference', + action: function (args, data, options, children, render) { + var compiled = compiledById(args[0], options); + data = getObject(args.length > 1 ? args[1] : defaultArg, data, options); + return render(compiled, data, options); + } + }, + '.': { + doc: 'Variable declaration', + action: function (args, data, options, children, render) { + options.context[args[0]] = getObject(args[1], data, options); + //TODO: allow definining a variable then doing a block with + //that variable. + return ''; + } + }, + '>': { + doc: 'Else', + action: function (args, data, options, children, render) { + if (children) { + return render(children, data, options); + } + return ''; + } + } + }; + + jig = function (text, data, options) { + var id; + if (typeof text === 'string') { + if (text.charAt(0) === '#') { + //a lookup by template ID + id = text.substring(1, text.length); + text = compiledById(id, options); + } else { + text = jig.compile(text, options); + } + } + return jig.render(text, data, options); + }; + + jig.htmlEscape = function (text) { + return text.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); + }; + + function compile(text, options) { + var compiled = [], + start = 0, + useRawHtml = false, + controlId = 0, + segment, index, match, tag, command, args, lastArg, lastChar, + children, i, tempTag; + + while ((index = text.indexOf(options.startToken, start)) !== -1) { + //Output any string that is before the template tag start + if (index !== start) { + compiled.push(text.substring(start, index)); + } + + //Find the end of the token + segment = text.substring(index); + match = options.endRegExp.exec(segment); + if (!match) { + //Just a loose start thing could be a regular punctuation. + compiled.push(segment); + return compiled; + } else { + //Command Match! + + //Increment start past the match. + start = index + match[0].length; + + //Pull out the command + tag = text.substring(index + options.startToken.length, index + match[0].length - options.endToken.length).trim(); + + //decode in case the value was in an URL field, like an href or an img src attribute + tag = decode(tag); + + //if the command is commented out end block call, that messes with stuff, + //just throw to let the user know, otherwise browser can lock up. + if (badCommentRegExp.test(tag)) { + throw new Error('blade/jig: end block tags should not be commented: ' + tag); + } + + command = tag.charAt(0); + + if (command === ']' && controlId) { + //In a control block, previous block was a related control block, + //so parse it without the starting ] character. + tempTag = tag.substring(1).trim(); + if (tempTag === '[') { + command = '>'; + } else { + command = tempTag.charAt(0); + //Remove the starting ] so it is seen as a regular tag + tag = tempTag; + } + } + + if (command && !options.propertyRegExp.test(command)) { + //Have a template command + tag = tag.substring(1).trim(); + } else { + command = '_default_'; + //Command could contain just the raw HTML indicator. + useRawHtml = (command === options.rawHtmlToken); + } + + //Allow for raw HTML output, but it is not the default. + //template references use raw by default though. + if ((useRawHtml = tag.indexOf(options.rawHtmlToken) === 0)) { + tag = tag.substring(options.rawHtmlToken.length, tag.length); + } + //However, template references use raw always + if (command === templateRefToken) { + useRawHtml = true; + } + + args = tag.split(options.argSeparator); + lastArg = args[args.length - 1]; + lastChar = lastArg.charAt(lastArg.length - 1); + children = null; + + if (command === ']') { + //If there are no other args, this is an end tag, to close + //out a block and possibly a set of control blocks. + if (lastChar !== '[') { + //End of a block. End the recursion, let the parent know + //the place where parsing stopped. + compiled.templateEnd = start; + + //Also end of a control section, indicate it as such. + compiled.endControl = true; + } else { + //End of a block. End the recursion, let the parent know + //the place where parsing stopped, before this end tag, + //so it can process it and match it to a control flow + //from previous control tag. + compiled.templateEnd = start - match[0].length; + } + + return compiled; + } else if (lastChar === '[') { + //If last arg ends with a [ it means a block element. + + //Assign a new control section ID if one is not in play already + if (!controlId) { + controlId = controlIdCounter++; + } + + //Adjust the last arg to not have the block character. + args[args.length - 1] = lastArg.substring(0, lastArg.length - 1); + + //Process the block + children = compile(text.substring(start), options); + + //Skip the part of the string that is part of the child compile. + start += children.templateEnd; + } + + //If this defines a template, save it off, + //if a comment (starts with /), then ignore it. + if (command === '+') { + options.templates[args[0]] = children; + } else if (command !== '/') { + //Adjust args if some end in commas, it means they are function + //args. + if (args.length > 1) { + for (i = args.length - 1; i >= 0; i--) { + if (args[i].charAt(args[i].length - 1) === ',') { + args[i] = args[i] + args[i + 1]; + args.splice(i + 1, 1); + } + } + } + + compiled.push({ + action: options.commands[command].action, + useRawHtml: useRawHtml, + args: args, + controlId: controlId, + children: children + }); + } + + //If the end of a block, clear the control ID + if (children && children.endControl) { + controlId = 0; + } + } + } + + if (start !== text.length - 1) { + compiled.push(text.substring(start, text.length)); + } + + return compiled; + } + + jig.compile = function (text, options) { + //Mix in defaults + options = options || {}; + object.mixin(options, { + startToken: startToken, + endToken: endToken, + rawHtmlToken: rawHtmlToken, + propertyRegExp: propertyRegExp, + commands: commands, + argSeparator: argSeparator, + templates: templateCache + }); + + options.endRegExp = new RegExp('[^\\r\\n]*?' + endToken); + + //Do some reset to avoid a number from getting too big. + controlIdCounter = 1; + + return compile(text, options); + }; + + /** + * Converts a node to a compiled template, and will store it in the cache. If already + * in the cache, it will give back the cached value. + */ + function nodeToCompiled(node, options) { + var text, compiled, clss, + id = node.id, + cache = options.templates || templateCache; + + //If the nodes has already been cached, then just get the cached value. + if (cache[id]) { + return cache[id]; + } + + //Call listener to allow processing of the node before + //template complication happens. + if (options.onBeforeParse) { + options.onBeforeParse(node); + } + + if (node.nodeName.toUpperCase() === 'SCRIPT') { + text = node.text.trim(); + if (node.parentNode) { + node.parentNode.removeChild(node); + } + } else { + //Put node in temp node to get the innerHTML so node's element + //html is in the output. + tempNode.appendChild(node); + + //Remove the id node and the template class, since this + //template text could be duplicated many times, and a + //template class is no longer useful. + node.removeAttribute('id'); + clss = (node.getAttribute('class') || '').trim(); + if (clss) { + node.setAttribute('class', clss.replace(templateClassRegExp, '$1$3')); + } + + //Decode braces when may get URL encoded as part of hyperlinks + text = tempNode.innerHTML.replace(/%7B/g, '{').replace(/%7D/g, '}'); + + //Clear out the temp node for the next use. + tempNode.removeChild(node); + } + compiled = jig.compile(text, options); + jig.cache(id, compiled, options); + return compiled; + } + + /** + * Parses an HTML document for templates, compiles them, and stores them + * in a cache of templates to use on the page. Only useful in browser environments. + * Script tags with type="text/template" are parsed, as well as DOM elements + * that have a class of "template" on them. The found nodes will be removed + * from the DOM as part of the parse operation. + * + * @param {Array-Like} [nodes] An array-like list of nodes. Could be a NodeList. + * @param {Object} [options] A collection of options to use for compilation. + */ + jig.parse = function (nodes, options) { + //Allow nodes to not be passed in, but still have options. + if (nodes && !nodes.length) { + options = nodes; + nodes = null; + } + + options = options || {}; + nodes = nodes || document.querySelectorAll('.template, script[type="text/template"]'); + + var node, i; + + for (i = nodes.length - 1; i > -1 && (node = nodes[i]); i--) { + nodeToCompiled(node, options); + } + }; + + function render(compiled, data, options) { + var text = '', i, dataId, controlId, currentControlId, currentValue, lastValue; + if (typeof compiled === 'string') { + text = compiled; + } else if (isArray(compiled)) { + for (i = 0; i < compiled.length; i++) { + //Account for control blocks (if/elseif/else) + //control blocks all have the same control ID, so only call the next + //control block if the first one did not return a value. + currentControlId = compiled[i].controlId; + if (!currentControlId || currentControlId !== controlId || !lastValue) { + currentValue = render(compiled[i], data, options); + text += currentValue; + if (currentControlId) { + controlId = currentControlId; + lastValue = currentValue; + } + } + } + } else { + //A template command to run. + text = compiled.action(compiled.args, data, options, compiled.children, render); + if (!text) { + text = ''; + } else if (!compiled.useRawHtml && !compiled.children) { + //Only html escape commands that are not block actions. + text = jig.htmlEscape(text.toString()); + } + } + + if (options.attachData) { + if (startTagRegExp.test(text)) { + dataId = 'id' + (dataIdCounter++); + text = text.replace(startTagRegExp, '$& data-blade-jig="' + dataId + '" '); + dataRegistry[dataId] = data; + } + } + + return text; + } + + /** + * Render a compiled template. + * + * @param {Array} compiled a compiled template + * @param {Object} data the data to use in the template + * @param {Object} options options for rendering. They include: + * @param {Object} templates a cache of compiled templates that might be + * referenced by the primary template + * @param {Object} options.fn a set of functions that might be used + * by the template(s). Each property on this object is a name of a function + * that may show up in the templates, and the value should be the function + * definition. + * @returns {String} the rendered template. + */ + jig.render = function (compiled, data, options) { + var i, result = ''; + + //Normalize options, filling in defaults. + options = options || {}; + object.mixin(options, { + templates: templateCache, + attachData: attachData, + strict: jig.strict + }); + + //Mix in default functions + if (options.fn) { + object.mixin(options.fn, defaultFuncs); + } else { + options.fn = defaultFuncs; + } + + //Mix in top level context object + options.context = options.context || object.create(data); + + //If data is an array, then render should be called for each item + //in the array. + if (isArray(data)) { + for (i = 0; i < data.length; i++) { + result += render(compiled, data[i], options); + } + return result; + } + + //Default case, just render + return render(compiled, data, options); + }; + + /** + * Enable strict template rendering checks. If a property does not exist on a + * data object, then an error will be logged. + */ + jig.strict = false; + + /** + * Track errors by logging to console if available. + */ + jig.error = typeof console !== 'undefined' && console.error ? + function (msg) { + console.error(msg); + } + : function () {}; + + /** + * Adds functions to the default set of functions that can be used inside + * a template. Newer definitions of a function will take precedence + * over the previously registered function. + * @param {Object} an object whose properties are names of functions + * and values are the functions that correspond to the names. + */ + jig.addFn = function (obj) { + object.mixin(defaultFuncs, obj, true); + }; + + /** + * Gets and sets the data bound to a particular rendered template. Setting + * the data does not change the already rendered template. + * + * @param {String||DOMNode} dataId the data ID, or a DOM node with a + * data-blade-jig attribute that was generated from a rendered template. + * @returns {Object} the bound data. Can return undefined if there is + * no data stored with that ID. + */ + jig.data = function (dataId, value) { + if (typeof dataId !== 'string') { + //Should be a DOM node or node list if it is not already a string. + if (!dataId.nodeType) { + dataId = dataId[0]; + } + dataId = dataId.getAttribute('data-blade-jig'); + } + + if (value !== undefined) { + return (dataRegistry[dataId] = value); + } else { + return dataRegistry[dataId]; + } + }; + + /** + * Removes some data that was bound to a rendered template. + * @param {String} dataId the data ID. It can be fetched from the + * data-blade-jig attribute on a rendered template. + */ + jig.removeData = function (dataId) { + delete dataRegistry[dataId]; + }; + + /** + * Gets an object given a string representation. For example, + * jig.getObject('foo.bar', baz) will return the baz.foo.bar value. + * + * @param {String} name the string value to fetch. The following formats + * are allowed: 'foo.bar', 'foo['bar']', 'foo[0]', 'foo[2:6]'. The last one + * will return an array subset. Functions are also supported: 'doSomething(foo.bar)' + * but the doSomething function needs to be defined in the options.fn + * property, as options.fn.doSomething = function (){} + * + * @param {Object} data the object to use as the basis for the object lookup. + * + * @param {Object} options. Options to the lookup. The only supported option + * at this time is options.func, and object defining functions can could be + * called. + * + * @returns {Object} it could return null if the name is not found off the data + */ + jig.getObject = getObject; + + /** + * Gets or sets a compiled template from a template cache. + * @param {String} id the template ID + * @param {String} [value] A string to compile to a template, or + * the compiled template value. + * @param {Object} [options] optional options object with a 'templates' + * property that contains some cached templates. If provided, a matching + * cache value for the ID will be used from options.templates, otherwise, + * the ID will be used to look up in the global blade/jig template cache. + * @returns {Object} a compiled template. It could return undefined if + * not match is found. + */ + jig.cache = function (id, value, options) { + //Convert the value to a compiled templated if necessary. + if (typeof value === 'string') { + value = jig.compile(value, options); + } + + //If value is not an array, then a get operation, likely an options. + if (!isArray(value)) { + options = value; + value = undefined; + } + + var cache = (options && options.templates) || templateCache; + if (value !== undefined) { + cache[id] = value; + } + + //Return the value. For get use, the template may not be in + //the local options.templates, but in the global cache, so + //be sure to check both. + return cache[id] || templateCache[id]; + }; + + function addToJQuery(jQuery) { + //Only handles queries where it is by a node ID, '#something'. + jQuery.fn.jig = function (data, options) { + //Convert this, which is a DOM node into a string of data + options = options || {}; + + var id = this.selector, + compiled; + + if (id.charAt(0) !== '#') { + throw new Error('blade/jig: only ID selectors, like "#something" are allowed with jig()'); + } + id = id.substring(1, id.length); + + //See if the template is already compiled. + compiled = (options.templates || templateCache)[id]; + + if (!compiled) { + compiled = nodeToCompiled(this[0]); + } + + return jQuery(jig.render(compiled, data, options)); + }; + } + + //Set up the plugin with a RequireJS-aware jQuery module but also + //if there is a global jQuery. + //require.modify('jquery', 'jquery-jig', ['jquery'], addToJQuery); + if (typeof jQuery !== 'undefined') { + addToJQuery(jQuery); + } + + return jig; +}); + diff --git a/web/scripts/blade/object.js b/web/scripts/blade/object.js new file mode 100644 index 0000000..6583610 --- /dev/null +++ b/web/scripts/blade/object.js @@ -0,0 +1,128 @@ +/** + * @license blade/object Copyright (c) 2010, The Dojo Foundation All Rights Reserved. + * Available via the MIT, GPL or new BSD license. + * see: http://github.com/jrburke/blade for details + */ +/*jslint plusplus: false */ +/*global require: false */ + +'use strict'; + +require.def('blade/object', function () { + + var empty = {}, + + /** + * Creates a new constructor function for generating objects of a certain type. + * + * @param {Object} base the base object to inherit from in the + * prototype chain. Pass null if no parent desired. + * + * @param {Array} mixins an array of objects to use to mix in their + * properties into the new object. Pass null if no mixins desired. + * + * @param {Function} objPropertyFunc, a function that returns an object + * whose properties should be part of this new object's prototype. + * The function will be passed the function used to call methods + * on the parent prototype used for this object. The function expects + * three arguments: + * - obj: pass the this object for this arg + * - funcName: the function name to call on the prototype object (a string) + * - args: an array of arguments. Normally just pass the arguments object. + * The parent prototype will be a combination of the base object + * with all mixins applied. + * + * @returns {Function} a constructor function. + */ + object = function (base, mixins, objPropertyFunc) { + var constructor, + + //Create the parent and its parentFunc calling wrapper. + //The parent function just makes it easier to call the parent + parent = object.create(base.prototype, mixins), + parentFunc = function (obj, funcName, args) { + return parent[funcName].apply(obj, args); + }, + + //Create a different object for the prototype instead of using + //parent, so that parent can still refer to parent object + //without the curren object's properties mixed in + //(via the objPropertyFunc) with the mixed in properties taking + //priority over the parent's properties. + proto = object.create(parent); + + object.mixin(proto, objPropertyFunc(parentFunc), true); + + //Create the constructor function. Calls init if it is defined + //on the prototype (proto) + constructor = function () { + //Protect against a missing new + if (!(this instanceof constructor)) { + throw new Error('blade/object: constructor function called without "new" in front'); + } + + //Call initializer if present. + if (this.init) { + this.init.apply(this, arguments); + } + }; + + //Set the prototype for this constructor + constructor.prototype = proto; + + return constructor; + }; + + /** + * Similar to ES5 create, but instead of setting property attributes + * for the second arg, allow an array of mixins to mix in properties + * to the newly created object. + * A copy of dojo.delegate + * @param {Object} parent the parent object to use as the prototype. + * @param {Array} [mixins] array of mixin objects to mix in to the new object. + */ + function Temp() {} + + object.create = function (obj, mixins) { + Temp.prototype = obj; + var temp = new Temp(), i, mixin; + + //Avoid any extra memory hanging around + Temp.prototype = null; + + if (mixins) { + for (i = 0; (mixin = mixins[i]); i++) { + object.mixin(temp, mixin); + } + } + return temp; // Object + }; + + /** + * Simple function to mix in properties from source into target, + * but only if target does not already have a property of the same name, + * unless override is set to true. Borrowed from Dojo. + * + * To extend a prototype on a given object, pass in the prototype property + * to mixin. For example: object.mixin(func.prototype, {a: 'b'}); + * + * @param {Object} target the object receiving the mixed in properties. + * + * @param {Object} source the object that contains the properties to mix in. + * + * @param {Boolean} [override] if set to true, then the source's properties + * will be mixed in even if a property of the same name already exists on + * the target. + */ + object.mixin = function (target, source, override) { + //TODO: consider ES5 getters and setters in here. + for (var prop in source) { + if (!(prop in empty) && (!(prop in target) || override)) { + target[prop] = source[prop]; + } + } + return require; + }; + + return object; +}); diff --git a/web/scripts/blade/url.js b/web/scripts/blade/url.js new file mode 100644 index 0000000..859b253 --- /dev/null +++ b/web/scripts/blade/url.js @@ -0,0 +1,59 @@ +/** + * @license blade/url Copyright (c) 2010, The Dojo Foundation All Rights Reserved. + * Available via the MIT, GPL or new BSD license. + * see: http://github.com/jrburke/blade for details + */ +/*jslint nomen: false, plusplus: false */ +/*global require: false */ + +'use strict'; + +require.def('blade/url', function () { + var ostring = Object.prototype.toString; + + return { + queryToObject: function (/*String*/ str) { + // summary: + // Create an object representing a de-serialized query section of a + // URL. Query keys with multiple values are returned in an array. + // + // example: + // This string: + // + // | "foo=bar&foo=baz&thinger=%20spaces%20=blah&zonk=blarg&" + // + // results in this object structure: + // + // | { + // | foo: [ "bar", "baz" ], + // | thinger: " spaces =blah", + // | zonk: "blarg" + // | } + // + // Note that spaces and other urlencoded entities are correctly + // handled. + var ret = {}, + qp = str.split('&'), + dec = decodeURIComponent, + parts, name, val; + + qp.forEach(function (item) { + if (item.length) { + parts = item.split('='); + name = dec(parts.shift()); + val = dec(parts.join('=')); + if (typeof ret[name] === 'string') { + ret[name] = [ret[name]]; + } + + if (ostring.call(ret[name]) === '[object Array]') { + ret[name].push(val); + } else { + ret[name] = val; + } + } + }); + return ret; + } + }; +}); diff --git a/web/scripts/cards.js b/web/scripts/cards.js new file mode 100644 index 0000000..8bfb634 --- /dev/null +++ b/web/scripts/cards.js @@ -0,0 +1,149 @@ +/*jslint */ +/*global require: false, window: false, document: false, cards: true */ +'use strict'; + +require.def('cards', ['jquery', 'text!templates/cardsHeader.html'], function ($, headerTemplate) { + var header, display, back, nlCards, + cardPosition = 0, + headerText = '', + cardTitles = []; + + function adjustCardSizes() { + var cardWidth = display.outerWidth(), + cardList = $('.card'), + totalWidth = cardWidth * cardList.length, + height = window.innerHeight - header.outerHeight(); + + //Set height + display.css('height', height + 'px'); + + //Set widths and heights of cards. Need to set the heights + //explicitly so any card using iscroll will get updated correctly. + nlCards.css({ + width: totalWidth + 'px', + height: height + 'px' + }); + + cardList.css({ + width: cardWidth + 'px', + height: height + 'px' + }); + + //Reset the scroll correctly. + cards.scroll(); + } + + function cards(nl, options) { + nl = nl.jquery ? nl : $(nl); + + $(function () { + //Insert the header before the cards + header = $(headerTemplate).insertBefore(nl); + headerText = $('#headerText'); + + back = $('#back'); + back.css('display', 'none'); + back.click((options && options.onBack) || cards.back); + + display = nl; + nlCards = display.find('#cards'); + + adjustCardSizes(); + cards.setTitle(options && options.title); + + //Detect orientation changes and size the card container size accordingly. + if ('onorientationchange' in window) { + window.addEventListener('orientationchange', adjustCardSizes, false); + } + window.addEventListener('resize', adjustCardSizes, false); + + }); + } + + cards.adjustCardSizes = adjustCardSizes; + + /** + * Adds a new card to the list of cards, at the end of the cards. + * Only adds the card, does not navigate to it. Only adds the card + * if a DOM element with the info.id does not already exist in the page. + * + * @param {Object} info the info about the card. It must have the following + * properties: + * @param {String} info.id the ID to use for the new card's DOM element. + * @param {String} info.title the text title to use for the card. + * @param {String} info.content a string of HTML to use for the content. + */ + cards.add = function (info) { + var existing = $('#' + info.id), + title = info.title; + + if (!title) { + title = info.content.match(/

([^<]+)<\/h1>/); + title = (title && title[1]) || ''; + } + + if (!existing.length) { + existing = $('
' + info.content + '
') + .appendTo('#cards'); + cards.adjustCardSizes(); + } + + return existing[0]; + }; + + cards.back = function () { + cardPosition -= 1; + if (cardPosition < 0) { + cardPosition = 0; + } + cards.scroll(); + }; + + cards.moveTo = function (id) { + cardPosition = $('.card').index(document.getElementById(id)); + if (cardPosition < 0) { + cardPosition = 0; + } + cards.scroll(); + }; + + cards.forward = function (title) { + cardPosition += 1; + cards.scroll(title); + }; + + cards.scroll = function (title) { + if (title) { + cardTitles[cardPosition] = title; + } + + cards.setTitle(title); + + var left = display.outerWidth() * cardPosition; + + nlCards.animate( + { + left: '-' + left + 'px' + }, { + duration: 300, + easing: 'linear' + } + ); + +/* + Was used for CSS -webkit-transition + nlCards.css({ + left: '-' + left + 'px' + }); +*/ + //Hide/Show back button as appropriate + back.css('display', !cardPosition ? 'none' : ''); + }; + + cards.setTitle = function (title) { + title = title || cardTitles[cardPosition] || nlCards.find('.card').eq(cardPosition).attr('title') || ''; + headerText.html(title); + }; + + return cards; +}); diff --git a/web/scripts/fancyzoom.js b/web/scripts/fancyzoom.js new file mode 100644 index 0000000..173c87a --- /dev/null +++ b/web/scripts/fancyzoom.js @@ -0,0 +1,174 @@ +(function($){ +$.fn.fancyZoom = function(options){ + + var options = options || {}; + var directory = options && options.directory ? options.directory : 'images'; + var zooming = false; + + if ($('#zoom').length == 0) { + var ext = $.browser.msie ? 'gif' : 'png'; + var html = ''; + + $('body').append(html); + + $('html').click(function(e){if($(e.target).parents('#zoom:visible').length == 0) hide();}); + $(document).keyup(function(event){ + if (event.keyCode == 27 && $('#zoom:visible').length > 0) hide(); + }); + + $('#zoom_close').click(hide); + } + + var zoom = $('#zoom'); + var zoom_table = $('#zoom_table'); + var zoom_close = $('#zoom_close'); + var zoom_content = $('#zoom_content'); + var middle_row = $('td.ml,td.mm,td.mr'); + + this.each(function(i) { + $($(this).attr('href')).hide(); + $(this).click(show); + }); + + return this; + + function show(e) { + if (zooming) return false; + zooming = true; + var content_div = $($(this).attr('href')); + var zoom_width = options.width; + var zoom_height = options.height; + + var width = window.innerWidth || (window.document.documentElement.clientWidth || window.document.body.clientWidth); + var height = window.innerHeight || (window.document.documentElement.clientHeight || window.document.body.clientHeight); + var x = window.pageXOffset || (window.document.documentElement.scrollLeft || window.document.body.scrollLeft); + var y = window.pageYOffset || (window.document.documentElement.scrollTop || window.document.body.scrollTop); + var window_size = {'width':width, 'height':height, 'x':x, 'y':y} + + var width = (zoom_width || content_div.width()) + 60; + var height = (zoom_height || content_div.height()) + 60; + var d = window_size; + + // ensure that newTop is at least 0 so it doesn't hide close button + var newTop = Math.max((d.height/2) - (height/2) + y, 0); + var newLeft = (d.width/2) - (width/2); + var curTop = e.pageY; + var curLeft = e.pageX; + + zoom_close.attr('curTop', curTop); + zoom_close.attr('curLeft', curLeft); + zoom_close.attr('scaleImg', options.scaleImg ? 'true' : 'false'); + + $('#zoom').hide().css({ + position : 'absolute', + top : curTop + 'px', + left : curLeft + 'px', + width : '1px', + height : '1px' + }); + + fixBackgroundsForIE(); + zoom_close.hide(); + + if (options.closeOnClick) { + $('#zoom').click(hide); + } + + if (options.scaleImg) { + zoom_content.html(content_div.html()); + $('#zoom_content img').css('width', '100%'); + } else { + zoom_content.html(''); + } + + $('#zoom').animate({ + top : newTop + 'px', + left : newLeft + 'px', + opacity : "show", + width : width, + height : height + }, 100, null, function() { + if (options.scaleImg != true) { + zoom_content.html(content_div.html()); + } + unfixBackgroundsForIE(); + zoom_close.show(); + zooming = false; + }) + return false; + } + + function hide() { + if (zooming) return false; + zooming = true; + $('#zoom').unbind('click'); + fixBackgroundsForIE(); + if (zoom_close.attr('scaleImg') != 'true') { + zoom_content.html(''); + } + zoom_close.hide(); + $('#zoom').animate({ + top : zoom_close.attr('curTop') + 'px', + left : zoom_close.attr('curLeft') + 'px', + opacity : "hide", + width : '1px', + height : '1px' + }, 100, null, function() { + if (zoom_close.attr('scaleImg') == 'true') { + zoom_content.html(''); + } + unfixBackgroundsForIE(); + zooming = false; + }); + return false; + } + + function switchBackgroundImagesTo(to) { + $('#zoom_table td').each(function(i) { + var bg = $(this).css('background-image').replace(/\.(png|gif|none)\"\)$/, '.' + to + '")'); + $(this).css('background-image', bg); + }); + var close_img = zoom_close.children('img'); + var new_img = close_img.attr('src').replace(/\.(png|gif|none)$/, '.' + to); + close_img.attr('src', new_img); + } + + function fixBackgroundsForIE() { + if ($.browser.msie && parseFloat($.browser.version) >= 7) { + switchBackgroundImagesTo('gif'); + } + } + + function unfixBackgroundsForIE() { + if ($.browser.msie && $.browser.version >= 7) { + switchBackgroundImagesTo('png'); + } + } +} +})(jQuery); \ No newline at end of file diff --git a/web/scripts/friendly.js b/web/scripts/friendly.js new file mode 100644 index 0000000..b3330cd --- /dev/null +++ b/web/scripts/friendly.js @@ -0,0 +1,129 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (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.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Raindrop. + * + * The Initial Developer of the Original Code is + * Mozilla Messaging, Inc.. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * */ + +/*jslint plusplus: false, nomen: false */ +/*global require: false */ +"use strict"; + +require.def("friendly", function () { + var friendly = { + timestamp: function (timestamp) { + return friendly.date(new Date(timestamp * 1000)); + }, + + date: function (date) { + var diff = (((new Date()).getTime() - date.getTime()) / 1000), + day_diff = Math.floor(diff / 86400), + dObj = { "friendly" : date.toLocaleDateString(), + "additional" : date.toLocaleTimeString(), + "utc" : date.toUTCString(), + "locale" : date.toLocaleString() }; + + /* some kind of error */ + if (day_diff < 0) { + dObj.friendly = "in the future"; + return dObj; + } else if (isNaN(day_diff)) { + dObj.friendly = dObj.additional = "unknown"; + return dObj; + } + + if (day_diff === 0) { + if (diff < 60) { + dObj.friendly = "just now"; + return dObj; + } + if (diff < 120 + 30) { /* 1 minute plus some fuzz */ + dObj.friendly = "a minute ago"; + return dObj; + } + if (diff < 3600) { + dObj.friendly = Math.floor(diff / 60) + " minutes ago"; + return dObj; + } + if (diff < (60 * 60) * 2) { + dObj.friendly = "1 hour ago"; + return dObj; + } + if (diff < 24 * 60 * 60) { + dObj.friendly = Math.floor(diff / 3600) + " hours ago"; + return dObj; + } + } + if (day_diff === 1) { + dObj.friendly = "yesterday"; + return dObj; + } + if (day_diff < 7) { + dObj.friendly = day_diff + " days ago"; + return dObj; + } + if (day_diff < 8) { + dObj.friendly = "last week"; + return dObj; + } + /* for this scope: we want day of week and the date + plus the month (if different) */ + if (day_diff < 31) { + dObj.friendly = Math.ceil(day_diff / 7) + " weeks ago"; + return dObj; + } + + /* for this scope: we want month + date */ + if (day_diff < 62) { + dObj.friendly = "a month ago"; + return dObj; + } + if (day_diff < 365) { + dObj.friendly = Math.ceil(day_diff / 31) + " months ago"; + return dObj; + } + + /* for this scope: we want month + year */ + if (day_diff >= 365 && day_diff < 730) { + dObj.additional = date.toLocaleDateString(); + dObj.friendly = "a year ago"; + return dObj; + } + if (day_diff >= 365) { + dObj.additional = date.toLocaleDateString(); + dObj.friendly = Math.ceil(day_diff / 365) + " years ago"; + return dObj; + } + return dObj; + }, + + name: function (name) { + var firstName = name.split(' ')[0]; + if (firstName.indexOf('@') !== -1) { + firstName = firstName.split('@')[0]; + } + firstName = firstName.replace(" ", ""); + firstName = firstName.replace("'", ""); + firstName = firstName.replace('"', ""); + return firstName; + } + }; + + return friendly; +}); diff --git a/web/scripts/hashDispatch.js b/web/scripts/hashDispatch.js new file mode 100644 index 0000000..d798121 --- /dev/null +++ b/web/scripts/hashDispatch.js @@ -0,0 +1,42 @@ +'use strict'; +/*jslint */ +/*global require: false, location: true, window: false */ + +require.def('hashDispatch', function (object) { + + /** + * Registers an object to receive hash changes. Expects the object + * to have property names that match the hash values. + * It will call the actions object immediately if the current hash + * matches a method on the object. + * + * @param {Object} actions the object that has property names that + * map to hash values and values for the properties are functions + * to be called. The name '_default' is used for any default action + * (one that corresponds to no hash value). A name of '_catchAll' is + * used to catch hash changes that do not map to a specific named + * action on the actions object. + */ + return function hashDispatch(actions) { + + function hashUpdated() { + var hash = (location.href.split('#')[1] || '_default'), + arg, index; + //Only use the part of the hash before a colon to find the action + index = hash.indexOf(':'); + if (index !== -1) { + arg = hash.substring(index + 1, hash.length); + hash = hash.substring(0, index); + } + + if (hash in actions) { + actions[hash](arg); + } else if (actions._catchAll) { + actions._catchAll(hash, arg); + } + } + + hashUpdated(); + window.addEventListener('hashchange', hashUpdated, false); + }; +}); diff --git a/web/scripts/iscroll-min.js b/web/scripts/iscroll-min.js new file mode 100644 index 0000000..78e8b55 --- /dev/null +++ b/web/scripts/iscroll-min.js @@ -0,0 +1 @@ +(function(){function a(f,c){this.element=typeof f=="object"?f:document.getElementById(f);this.wrapper=this.element.parentNode;this.is3d=("m11" in new WebKitCSSMatrix());var e="-webkit-transition-property:-webkit-transform;-webkit-transition-timing-function:cubic-bezier(0,0,0.25,1);-webkit-transition-duration:0;-webkit-transform:"+(this.is3d?"translate3d(0,0,0)":"translate(0,0)");this.element.setAttribute("style",e);this.options={bounce:this.is3d,momentum:this.is3d,checkDOMChanges:true,topOnDOMChanges:false,hScrollBar:true,vScrollBar:true,overflow:"auto"};if(typeof c=="object"){for(var d in c){this.options[d]=c[d]}}this.wrapper.style.overflow=this.options.overflow;this.refresh();window.addEventListener("orientationchange",this,false);this.element.addEventListener("touchstart",this,false);if(this.options.checkDOMChanges){this.element.addEventListener("DOMSubtreeModified",this,false)}}a.prototype={x:0,y:0,scrollBarTimeout:null,handleEvent:function(c){switch(c.type){case"touchstart":this.touchStart(c);break;case"touchmove":this.touchMove(c);break;case"touchend":this.touchEnd(c);break;case"webkitTransitionEnd":this.transitionEnd(c);break;case"orientationchange":this.refresh();break;case"DOMSubtreeModified":this.onDOMModified(c);break}},onDOMModified:function(c){this.refresh();if(this.options.topOnDOMChanges&&(this.x!=0||this.y!=0)){this.scrollTo(0,0,"0")}},refresh:function(){this.scrollWidth=this.wrapper.clientWidth;this.scrollHeight=this.wrapper.clientHeight;this.maxScrollX=this.scrollWidth-this.element.offsetWidth;this.maxScrollY=this.scrollHeight-this.element.offsetHeight;var d=this.x,c=this.y;if(this.scrollX){if(this.maxScrollX>=0){d=0}else{if(this.x=0){c=0}else{if(this.ythis.scrollWidth?true:false;this.scrollY=this.element.offsetHeight>this.scrollHeight?true:false;if(this.options.hScrollBar&&this.scrollX){this.scrollBarX=(this.scrollBarX instanceof b)?this.scrollBarX:new b("horizontal",this.wrapper);this.scrollBarX.init(this.scrollWidth,this.element.offsetWidth)}else{if(this.scrollBarX){this.scrollBarX=this.scrollBarX.remove()}}if(this.options.vScrollBar&&this.scrollY){this.scrollBarY=(this.scrollBarY instanceof b)?this.scrollBarY:new b("vertical",this.wrapper);this.scrollBarY.init(this.scrollHeight,this.element.offsetHeight)}else{if(this.scrollBarY){this.scrollBarY=this.scrollBarY.remove()}}},setPosition:function(c,e,d){this.x=c;this.y=e;this.element.style.webkitTransform=this.is3d?"translate3d("+this.x+"px,"+this.y+"px,0px)":"translate("+this.x+"px,"+this.y+"px)";if(!d){if(this.scrollBarX){this.scrollBarX.setPosition(this.x)}if(this.scrollBarY){this.scrollBarY.setPosition(this.y)}}},setTransitionTime:function(c){c=c||"0";this.element.style.webkitTransitionDuration=c;if(this.scrollBarX){this.scrollBarX.bar.style.webkitTransitionDuration=c+(this.is3d?",200ms":"")}if(this.scrollBarY){this.scrollBarY.bar.style.webkitTransitionDuration=c+(this.is3d?",200ms":"")}},touchStart:function(d){if(d.targetTouches.length!=1){return false}d.preventDefault();d.stopPropagation();this.setTransitionTime();if(this.options.momentum){var c=new WebKitCSSMatrix(window.getComputedStyle(this.element).webkitTransform);if(c.e!=this.x||c.f!=this.y){this.element.removeEventListener("webkitTransitionEnd",this,false);this.setPosition(c.e,c.f)}}if(this.scrollBarTimeout){clearTimeout(this.scrollBarTimeout);this.scrollBarTimeout=null}this.touchStartX=d.touches[0].pageX;this.scrollStartX=this.x;this.touchStartY=d.touches[0].pageY;this.scrollStartY=this.y;this.scrollStartTime=d.timeStamp;this.moved=false;this.element.addEventListener("touchmove",this,false);this.element.addEventListener("touchend",this,false)},touchMove:function(g){if(g.targetTouches.length!=1){return false}var d=this.scrollX===true?g.touches[0].pageX-this.touchStartX:0,c=this.scrollY===true?g.touches[0].pageY-this.touchStartY:0,h=this.x+d,f=this.y+c;if(h>0||h0||f250){this.scrollStartX=this.x;this.scrollStartY=this.y;this.scrollStartTime=g.timeStamp}},touchEnd:function(j){this.element.removeEventListener("touchmove",this,false);this.element.removeEventListener("touchend",this,false);var g=j.timeStamp-this.scrollStartTime;if(!this.moved){this.resetPosition();var k=j.changedTouches[0].target;while(k.nodeType!=1){k=k.parentNode}var l=document.createEvent("MouseEvents");l.initMouseEvent("click",true,true,j.view,1,k.screenX,k.screenY,k.clientX,k.clientY,j.ctrlKey,j.altKey,j.shiftKey,j.metaKey,0,null);k.dispatchEvent(l);return false}if(!this.options.momentum||g>250){this.resetPosition();return false}var d=this.scrollX===true?this.momentum(this.x-this.scrollStartX,g,this.options.bounce?-this.x+this.scrollWidth/3:-this.x,this.options.bounce?this.x+this.element.offsetWidth-this.scrollWidth+this.scrollWidth/3:this.x+this.element.offsetWidth-this.scrollWidth):{dist:0,time:0};var c=this.scrollY===true?this.momentum(this.y-this.scrollStartY,g,this.options.bounce?-this.y+this.scrollHeight/3:-this.y,this.options.bounce?this.y+this.element.offsetHeight-this.scrollHeight+this.scrollHeight/3:this.y+this.element.offsetHeight-this.scrollHeight):{dist:0,time:0};if(!d.dist&&!c.dist){this.resetPosition();return false}var f=Math.max(Math.max(d.time,c.time),1);var i=this.x+d.dist;var h=this.y+c.dist;this.scrollTo(i,h,f+"ms")},transitionEnd:function(){this.element.removeEventListener("webkitTransitionEnd",this,false);this.resetPosition()},resetPosition:function(e){var f=this.x,d=this.y,c=this,e=e||"600ms";if(this.x>=0){f=0}else{if(this.x=0){d=0}else{if(this.y0&&f>h){e=e*h/f;f=h}if(j<0&&f>c){e=e*c/f;f=c}f=f*(j<0?-1:1);k=e/i;return{dist:Math.round(f),time:Math.round(k)}}};var b=function(c,e){this.is3d=("m11" in new WebKitCSSMatrix());this.dir=c;this.bar=document.createElement("div");this.bar.className="scrollbar "+c;var d="-webkit-transition-timing-function:cubic-bezier(0,0,0.25,1);pointer-events:none;opacity:0;"+(this.is3d?"-webkit-transition-duration:0,200ms;-webkit-transition-property:-webkit-transform,opacity;-webkit-transform:translate3d(0,0,0)":"-webkit-transition-duration:0;-webkit-transition-property:webkit-transform;-webkit-transform:translate(0,0)");this.bar.setAttribute("style",d);e.appendChild(this.bar)};b.prototype={init:function(c,d){var e=this.dir=="horizontal"?this.bar.offsetWidth-this.bar.clientWidth:this.bar.offsetHeight-this.bar.clientHeight;this.maxSize=c-8;this.size=Math.round(this.maxSize*this.maxSize/d)+e;this.maxScroll=this.maxSize-this.size;this.toWrapperProp=this.maxScroll/(c-d);this.bar.style[this.dir=="horizontal"?"width":"height"]=(this.size-e)+"px"},setPosition:function(d,c){if(!c&&this.bar.style.opacity!="1"){this.show()}d=this.toWrapperProp*d;if(d<0){d=0}else{if(d>this.maxScroll){d=this.maxScroll}}if(this.is3d){d=this.dir=="horizontal"?"translate3d("+Math.round(d)+"px,0,0)":"translate3d(0,"+Math.round(d)+"px,0)"}else{d=this.dir=="horizontal"?"translate("+Math.round(d)+"px,0)":"translate(0,"+Math.round(d)+"px)"}this.bar.style.webkitTransform=d},show:function(){this.bar.style.opacity="1"},hide:function(){this.bar.style.opacity="0"},remove:function(){this.bar.parentNode.removeChild(this.bar);return null}};window.iScroll=a})(); \ No newline at end of file diff --git a/web/scripts/isoDate.js b/web/scripts/isoDate.js new file mode 100644 index 0000000..9fc18e4 --- /dev/null +++ b/web/scripts/isoDate.js @@ -0,0 +1,149 @@ +//Ported directly from dojo.date.stamp +'use strict'; +/*jslint nomen: false, regexp: false, plusplus: false */ +/*global require: false */ + +require('isoDate', function () { + + // Methods to convert dates to or from a wire (string) format using well-known conventions + var _isoRegExp = /^(?:(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(.\d+)?)?((?:[+\-](\d{2}):(\d{2}))|Z)?)?$/, + + isoDate = function (/*String*/formattedString, /*Number?*/defaultTime) { + // summary: + // Returns a Date object given a string formatted according to a subset of the ISO-8601 standard. + // + // description: + // Accepts a string formatted according to a profile of ISO8601 as defined by + // [RFC3339](http://www.ietf.org/rfc/rfc3339.txt), except that partial input is allowed. + // Can also process dates as specified [by the W3C](http://www.w3.org/TR/NOTE-datetime) + // The following combinations are valid: + // + // * dates only + // | * yyyy + // | * yyyy-MM + // | * yyyy-MM-dd + // * times only, with an optional time zone appended + // | * THH:mm + // | * THH:mm:ss + // | * THH:mm:ss.SSS + // * and "datetimes" which could be any combination of the above + // + // timezones may be specified as Z (for UTC) or +/- followed by a time expression HH:mm + // Assumes the local time zone if not specified. Does not validate. Improperly formatted + // input may return null. Arguments which are out of bounds will be handled + // by the Date constructor (e.g. January 32nd typically gets resolved to February 1st) + // Only years between 100 and 9999 are supported. + // + // formattedString: + // A string such as 2005-06-30T08:05:00-07:00 or 2005-06-30 or T08:05:00 + // + // defaultTime: + // Used for defaults for fields omitted in the formattedString. + // Uses 1970-01-01T00:00:00.0Z by default. + + var match = _isoRegExp.exec(formattedString), + result = null, offset, zoneSign; + + if (match) { + match.shift(); + if (match[1]) { + match[1]--; // Javascript Date months are 0-based + } + if (match[6]) { + match[6] *= 1000; // Javascript Date expects fractional seconds as milliseconds + } + + if (defaultTime) { + // mix in defaultTime. Relatively expensive, so use || operators for the fast path of defaultTime === 0 + defaultTime = new Date(defaultTime); + ["FullYear", "Month", "Date", "Hours", "Minutes", "Seconds", "Milliseconds"].map(function (prop) { + return defaultTime["get" + prop](); + }).forEach(function (value, index) { + match[index] = match[index] || value; + }); + } + result = new Date(match[0] || 1970, match[1] || 0, match[2] || 1, match[3] || 0, match[4] || 0, match[5] || 0, match[6] || 0); //TODO: UTC defaults + if (match[0] < 100) { + result.setFullYear(match[0] || 1970); + } + + offset = 0; + zoneSign = match[7] && match[7].charAt(0); + if (zoneSign !== 'Z') { + offset = ((match[8] || 0) * 60) + (Number(match[9]) || 0); + if (zoneSign !== '-') { + offset *= -1; + } + } + if (zoneSign) { + offset -= result.getTimezoneOffset(); + } + if (offset) { + result.setTime(result.getTime() + offset * 60000); + } + } + + return result; // Date or null + }; + + /*===== + __Options = function(){ + // selector: String + // "date" or "time" for partial formatting of the Date object. + // Both date and time will be formatted by default. + // zulu: Boolean + // if true, UTC/GMT is used for a timezone + // milliseconds: Boolean + // if true, output milliseconds + this.selector = selector; + this.zulu = zulu; + this.milliseconds = milliseconds; + } + =====*/ + + isoDate.toIsoString = function (/*Date*/dateObject, /*__Options?*/options) { + // summary: + // Format a Date object as a string according a subset of the ISO-8601 standard + // + // description: + // When options.selector is omitted, output follows [RFC3339](http://www.ietf.org/rfc/rfc3339.txt) + // The local time zone is included as an offset from GMT, except when selector=='time' (time without a date) + // Does not check bounds. Only years between 100 and 9999 are supported. + // + // dateObject: + // A Date object + + var _ = function (n) { + return (n < 10) ? "0" + n : n; + }, + formattedDate, getter, date, year, time, millis, timezoneOffset, absOffset; + options = options || {}; + formattedDate = []; + getter = options.zulu ? "getUTC" : "get"; + date = ""; + if (options.selector !== "time") { + year = dateObject[getter + "FullYear"](); + date = ["0000".substr((year + "").length) + year, _(dateObject[getter + "Month"]() + 1), _(dateObject[getter + "Date"]())].join('-'); + } + formattedDate.push(date); + if (options.selector !== "date") { + time = [_(dateObject[getter + "Hours"]()), _(dateObject[getter + "Minutes"]()), _(dateObject[getter + "Seconds"]())].join(':'); + millis = dateObject[getter + "Milliseconds"](); + if (options.milliseconds) { + time += "." + (millis < 100 ? "0" : "") + _(millis); + } + if (options.zulu) { + time += "Z"; + } else if (options.selector !== "time") { + timezoneOffset = dateObject.getTimezoneOffset(); + absOffset = Math.abs(timezoneOffset); + time += (timezoneOffset > 0 ? "-" : "+") + + _(Math.floor(absOffset / 60)) + ":" + _(absOffset % 60); + } + formattedDate.push(time); + } + return formattedDate.join('T'); // String + }; + + return isoDate; +}); \ No newline at end of file diff --git a/web/scripts/jquery.easing.1.3.js b/web/scripts/jquery.easing.1.3.js new file mode 100644 index 0000000..ef74321 --- /dev/null +++ b/web/scripts/jquery.easing.1.3.js @@ -0,0 +1,205 @@ +/* + * jQuery Easing v1.3 - http://gsgd.co.uk/sandbox/jquery/easing/ + * + * Uses the built in easing capabilities added In jQuery 1.1 + * to offer multiple easing options + * + * TERMS OF USE - jQuery Easing + * + * Open source under the BSD License. + * + * Copyright © 2008 George McGinley Smith + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * Neither the name of the author nor the names of contributors may be used to endorse + * or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * +*/ + +// t: current time, b: begInnIng value, c: change In value, d: duration +jQuery.easing['jswing'] = jQuery.easing['swing']; + +jQuery.extend( jQuery.easing, +{ + def: 'easeOutQuad', + swing: function (x, t, b, c, d) { + //alert(jQuery.easing.default); + return jQuery.easing[jQuery.easing.def](x, t, b, c, d); + }, + easeInQuad: function (x, t, b, c, d) { + return c*(t/=d)*t + b; + }, + easeOutQuad: function (x, t, b, c, d) { + return -c *(t/=d)*(t-2) + b; + }, + easeInOutQuad: function (x, t, b, c, d) { + if ((t/=d/2) < 1) return c/2*t*t + b; + return -c/2 * ((--t)*(t-2) - 1) + b; + }, + easeInCubic: function (x, t, b, c, d) { + return c*(t/=d)*t*t + b; + }, + easeOutCubic: function (x, t, b, c, d) { + return c*((t=t/d-1)*t*t + 1) + b; + }, + easeInOutCubic: function (x, t, b, c, d) { + if ((t/=d/2) < 1) return c/2*t*t*t + b; + return c/2*((t-=2)*t*t + 2) + b; + }, + easeInQuart: function (x, t, b, c, d) { + return c*(t/=d)*t*t*t + b; + }, + easeOutQuart: function (x, t, b, c, d) { + return -c * ((t=t/d-1)*t*t*t - 1) + b; + }, + easeInOutQuart: function (x, t, b, c, d) { + if ((t/=d/2) < 1) return c/2*t*t*t*t + b; + return -c/2 * ((t-=2)*t*t*t - 2) + b; + }, + easeInQuint: function (x, t, b, c, d) { + return c*(t/=d)*t*t*t*t + b; + }, + easeOutQuint: function (x, t, b, c, d) { + return c*((t=t/d-1)*t*t*t*t + 1) + b; + }, + easeInOutQuint: function (x, t, b, c, d) { + if ((t/=d/2) < 1) return c/2*t*t*t*t*t + b; + return c/2*((t-=2)*t*t*t*t + 2) + b; + }, + easeInSine: function (x, t, b, c, d) { + return -c * Math.cos(t/d * (Math.PI/2)) + c + b; + }, + easeOutSine: function (x, t, b, c, d) { + return c * Math.sin(t/d * (Math.PI/2)) + b; + }, + easeInOutSine: function (x, t, b, c, d) { + return -c/2 * (Math.cos(Math.PI*t/d) - 1) + b; + }, + easeInExpo: function (x, t, b, c, d) { + return (t==0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b; + }, + easeOutExpo: function (x, t, b, c, d) { + return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b; + }, + easeInOutExpo: function (x, t, b, c, d) { + if (t==0) return b; + if (t==d) return b+c; + if ((t/=d/2) < 1) return c/2 * Math.pow(2, 10 * (t - 1)) + b; + return c/2 * (-Math.pow(2, -10 * --t) + 2) + b; + }, + easeInCirc: function (x, t, b, c, d) { + return -c * (Math.sqrt(1 - (t/=d)*t) - 1) + b; + }, + easeOutCirc: function (x, t, b, c, d) { + return c * Math.sqrt(1 - (t=t/d-1)*t) + b; + }, + easeInOutCirc: function (x, t, b, c, d) { + if ((t/=d/2) < 1) return -c/2 * (Math.sqrt(1 - t*t) - 1) + b; + return c/2 * (Math.sqrt(1 - (t-=2)*t) + 1) + b; + }, + easeInElastic: function (x, t, b, c, d) { + var s=1.70158;var p=0;var a=c; + if (t==0) return b; if ((t/=d)==1) return b+c; if (!p) p=d*.3; + if (a < Math.abs(c)) { a=c; var s=p/4; } + else var s = p/(2*Math.PI) * Math.asin (c/a); + return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b; + }, + easeOutElastic: function (x, t, b, c, d) { + var s=1.70158;var p=0;var a=c; + if (t==0) return b; if ((t/=d)==1) return b+c; if (!p) p=d*.3; + if (a < Math.abs(c)) { a=c; var s=p/4; } + else var s = p/(2*Math.PI) * Math.asin (c/a); + return a*Math.pow(2,-10*t) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b; + }, + easeInOutElastic: function (x, t, b, c, d) { + var s=1.70158;var p=0;var a=c; + if (t==0) return b; if ((t/=d/2)==2) return b+c; if (!p) p=d*(.3*1.5); + if (a < Math.abs(c)) { a=c; var s=p/4; } + else var s = p/(2*Math.PI) * Math.asin (c/a); + if (t < 1) return -.5*(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b; + return a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )*.5 + c + b; + }, + easeInBack: function (x, t, b, c, d, s) { + if (s == undefined) s = 1.70158; + return c*(t/=d)*t*((s+1)*t - s) + b; + }, + easeOutBack: function (x, t, b, c, d, s) { + if (s == undefined) s = 1.70158; + return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b; + }, + easeInOutBack: function (x, t, b, c, d, s) { + if (s == undefined) s = 1.70158; + if ((t/=d/2) < 1) return c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b; + return c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b; + }, + easeInBounce: function (x, t, b, c, d) { + return c - jQuery.easing.easeOutBounce (x, d-t, 0, c, d) + b; + }, + easeOutBounce: function (x, t, b, c, d) { + if ((t/=d) < (1/2.75)) { + return c*(7.5625*t*t) + b; + } else if (t < (2/2.75)) { + return c*(7.5625*(t-=(1.5/2.75))*t + .75) + b; + } else if (t < (2.5/2.75)) { + return c*(7.5625*(t-=(2.25/2.75))*t + .9375) + b; + } else { + return c*(7.5625*(t-=(2.625/2.75))*t + .984375) + b; + } + }, + easeInOutBounce: function (x, t, b, c, d) { + if (t < d/2) return jQuery.easing.easeInBounce (x, t*2, 0, c, d) * .5 + b; + return jQuery.easing.easeOutBounce (x, t*2-d, 0, c, d) * .5 + c*.5 + b; + } +}); + +/* + * + * TERMS OF USE - EASING EQUATIONS + * + * Open source under the BSD License. + * + * Copyright © 2001 Robert Penner + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * Neither the name of the author nor the names of contributors may be used to endorse + * or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ \ No newline at end of file diff --git a/web/scripts/jquery.masonry.js b/web/scripts/jquery.masonry.js new file mode 100644 index 0000000..fbf8425 --- /dev/null +++ b/web/scripts/jquery.masonry.js @@ -0,0 +1,308 @@ +/************************************************* +** jQuery Masonry version 1.2.0 +** copyright David DeSandro, licensed GPL & MIT +** http://desandro.com/resources/jquery-masonry +**************************************************/ +;(function($){ + + /*! + * smartresize: debounced resize event for jQuery + * http://github.com/lrbabe/jquery-smartresize + * + * Copyright (c) 2009 Louis-Remi Babe + * Licensed under the GPL license. + * http://docs.jquery.com/License + * + */ + var event = $.event, + resizeTimeout; + + event.special[ "smartresize" ] = { + setup: function() { + $( this ).bind( "resize", event.special.smartresize.handler ); + }, + teardown: function() { + $( this ).unbind( "resize", event.special.smartresize.handler ); + }, + handler: function( event, execAsap ) { + // Save the context + var context = this, + args = arguments; + + // set correct event type + event.type = "smartresize"; + + if(resizeTimeout) + clearTimeout(resizeTimeout); + resizeTimeout = setTimeout(function() { + jQuery.event.handle.apply( context, args ); + }, execAsap === "execAsap"? 0 : 100); + } + }; + + $.fn.smartresize = function( fn ) { + return fn ? this.bind( "smartresize", fn ) : this.trigger( "smartresize", ["execAsap"] ); + }; + + + + // masonry code begin + $.fn.masonry = function(options, callback) { + + function getBricks(props, opts) { + props.$bricks = opts.itemSelector == undefined ? + opts.$brickParent.children() : + opts.$brickParent.find(opts.itemSelector); + } + + function placeBrick($brick, setCount, setY, setSpan, props, opts) { + var shortCol = 0; + + for ( i=0; i < setCount; i++ ) { + if ( setY[i] < setY[ shortCol ] ) shortCol = i; + } + + var position = { + left: props.colW * shortCol + props.posLeft, + top: setY[ shortCol ] + }; + + + if( props.masoned && opts.animate ) { + $brick.animate( position, { + duration: opts.animationOptions.duration, + easing: opts.animationOptions.easing, + complete: opts.animationOptions.complete, + step: opts.animationOptions.step, + queue: opts.animationOptions.queue, + specialEasing: opts.animationOptions.specialEasing + }); + } else { + $brick.css(position); + } + + for ( i=0; i < setSpan; i++ ) { + props.colY[ shortCol + i ] = setY[ shortCol ] + $brick.outerHeight(true) ; + } + }; + + + function masonrySetup($wall, opts, props) { + getBricks(props, opts); + + if ( opts.columnWidth == undefined) { + props.colW = props.masoned ? + $wall.data('masonry').colW : + props.$bricks.outerWidth(true); + } else { + props.colW = opts.columnWidth; + } + + props.colCount = Math.floor( $wall.width() / props.colW ) ; + props.colCount = Math.max( props.colCount, 1 ); + }; + + + function masonryArrange($wall, opts, props) { + // if masonry hasn't been called before + if( !props.masoned ) $wall.css( 'position', 'relative' ); + + if ( !props.masoned || opts.appendedContent != undefined ) { + // just the new bricks + props.$bricks.css( 'position', 'absolute' ); + } + + // get top left position of where the bricks should be + var cursor = $('
'); + $wall.prepend( cursor ); + props.posTop = Math.round( cursor.position().top ); + props.posLeft = Math.round( cursor.position().left ); + cursor.remove(); + + // set up column Y array + if ( props.masoned && opts.appendedContent != undefined ) { + // if appendedContent is set, use colY from last call + props.colY = $wall.data('masonry').colY; + + /* + * in the case that the wall is not resizeable, + * but the colCount has changed from the previous time + * masonry has been called + */ + for (i= $wall.data('masonry').colCount; i < props.colCount; i++) { + props.colY[i] = props.posTop; + }; + + } else { + props.colY = []; + for ( i=0; i < props.colCount; i++) { + props.colY[i] = props.posTop; + } + } + + + // layout logic + if ( opts.singleMode ) { + props.$bricks.each(function(){ + var $brick = $(this); + placeBrick($brick, props.colCount, props.colY, 1, props, opts); + }); + } else { + props.$bricks.each(function() { + var $brick = $(this); + + //how many columns does this brick span + var colSpan = Math.ceil( $brick.outerWidth(true) / props.colW); + colSpan = Math.min( colSpan, props.colCount ); + + + if ( colSpan == 1 ) { + // if brick spans only one column, just like singleMode + placeBrick($brick, props.colCount, props.colY, 1, props, opts); + } else { + // brick spans more than one column + + //how many different places could this brick fit horizontally + var groupCount = props.colCount + 1 - colSpan; + var groupY = [0]; + // for each group potential horizontal position + for ( i=0; i < groupCount; i++ ) { + groupY[i] = 0; + // for each column in that group + for ( j=0; j < colSpan; j++ ) { + // get the maximum column height in that group + groupY[i] = Math.max( groupY[i], props.colY[i+j] ); + } + } + + placeBrick($brick, groupCount, groupY, colSpan, props, opts); + } + }); // /props.bricks.each(function() { + } // /layout logic + + // set the height of the wall to the tallest column + props.wallH = 0; + for ( i=0; i < props.colCount; i++ ) { + props.wallH = Math.max( props.wallH, props.colY[i] ); + } + var wallCSS = { height: props.wallH - props.posTop }; + // $wall.height( props.wallH - props.posTop ); + if ( props.masoned && opts.animate ) { + $wall.animate(wallCSS, { + duration: opts.animationOptions.duration, + easing: opts.animationOptions.easing, + complete: opts.animationOptions.complete, + step: opts.animationOptions.step, + queue: opts.animationOptions.queue, + specialEasing: opts.animationOptions.specialEasing + }); + } else { + $wall.css(wallCSS); + } + + // add masoned class first time around + if ( !props.masoned ) $wall.addClass('masoned'); + + // provide props.bricks as context for the callback + callback.call( props.$bricks ); + + // set all data so we can retrieve it for appended appendedContent + // or anyone else's crazy jquery fun + $wall.data('masonry', props ); + + + }; // /masonryArrange function + + + function masonryResize($wall, opts, props) { + props.masoned = $wall.data('masonry') != undefined; + var prevColCount = $wall.data('masonry').colCount; + masonrySetup($wall, opts, props); + if ( props.colCount != prevColCount ) masonryArrange($wall, opts, props); + }; + + + /* + * let's begin + * IN A WORLD... + */ + return this.each(function() { + + var $wall = $(this); + + var props = $.extend( {}, $.masonry ); + + // checks if masonry has been called before on this object + props.masoned = $wall.data('masonry') != undefined; + + var previousOptions = props.masoned ? $wall.data('masonry').options : {}; + + var opts = $.extend( + {}, + props.defaults, + previousOptions, + options + ); + + // should we save these options for next time? + props.options = opts.saveOptions ? opts : previousOptions; + + //picked up from Paul Irish + callback = callback || function(){}; + + + if ( props.masoned && opts.appendedContent != undefined ) { + // if we're dealing with appendedContent + opts.$brickParent = opts.appendedContent; + } else { + opts.$brickParent = $wall; + } + + getBricks(props, opts); + + + if ( props.$bricks.length ) { + // call masonry layout + masonrySetup($wall, opts, props); + masonryArrange($wall, opts, props); + + // binding window resizing + var resizeOn = previousOptions.resizeable; + if ( !resizeOn && opts.resizeable ) { + $(window).bind('smartresize.masonry', function() { masonryResize($wall, opts, props); } ); + } + if ( resizeOn && !opts.resizeable ) $(window).unbind('smartresize.masonry'); + } else { + // brickParent is empty, do nothing, go back home and eat chips + return this; + } + + }); // /return this.each(function() + }; // /$.fn.masonry = function(options) + + + + $.masonry = { + defaults : { + singleMode: false, + columnWidth: undefined, + itemSelector: undefined, + appendedContent: undefined, + saveOptions: true, + resizeable: true, + animate: false, + animationOptions: {} + }, + colW: undefined, + colCount: undefined, + colY: undefined, + wallH: undefined, + masoned: undefined, + posTop: 0, + posLeft: 0, + options: undefined, + $bricks: undefined, + $brickParent: undefined + }; + +})(jQuery); \ No newline at end of file diff --git a/web/scripts/jquery.tmpl.js b/web/scripts/jquery.tmpl.js new file mode 100644 index 0000000..936a7bf --- /dev/null +++ b/web/scripts/jquery.tmpl.js @@ -0,0 +1,486 @@ + /* + * jQuery Templating Plugin + * Copyright 2010, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + */ +(function( jQuery, undefined ){ + var oldManip = jQuery.fn.domManip, tmplItmAtt = "_tmplitem", htmlExpr = /^[^<]*(<[\w\W]+>)[^>]*$/, + newTmplItems = {}, wrappedItems = {}, appendToTmplItems, topTmplItem = { key: 0, data: {} }, itemKey = 0, cloneIndex = 0, stack = []; + + function newTmplItem( options, parentItem, fn, data ) { + // Returns a template item data structure for a new rendered instance of a template (a 'template item'). + // The content field is a hierarchical array of strings and nested items (to be + // removed and replaced by nodes field of dom elements, once inserted in DOM). + var newItem = { + data: data || (parentItem ? parentItem.data : {}), + _wrap: parentItem ? parentItem._wrap : null, + tmpl: null, + parent: parentItem || null, + nodes: [], + calls: tiCalls, + nest: tiNest, + wrap: tiWrap, + html: tiHtml, + update: tiUpdate + }; + if ( options ) { + jQuery.extend( newItem, options, { nodes: [], parent: parentItem } ); + } + if ( fn ) { + // Build the hierarchical content to be used during insertion into DOM + newItem.tmpl = fn; + newItem._ctnt = newItem._ctnt || newItem.tmpl( jQuery, newItem ); + newItem.key = ++itemKey; + // Keep track of new template item, until it is stored as jQuery Data on DOM element + (stack.length ? wrappedItems : newTmplItems)[itemKey] = newItem; + } + return newItem; + } + + // Override appendTo etc., in order to provide support for targeting multiple elements. (This code would disappear if integrated in jquery core). + 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; + + appendToTmplItems = newTmplItems || {}; + if ( parent && parent.nodeType === 11 && parent.childNodes.length === 1 && insert.length === 1 ) { + insert[ original ]( this[0] ); + ret = this; + } else { + for ( var i = 0, l = insert.length; i < l; i++ ) { + cloneIndex = i; + var elems = (i > 0 ? this.clone(true) : this).get(); + jQuery.fn[ original ].apply( jQuery(insert[i]), elems ); + ret = ret.concat( elems ); + } + cloneIndex = 0; + ret = this.pushStack( ret, name, insert.selector ); + } + var tmplItems = appendToTmplItems; + appendToTmplItems = null; + jQuery.tmpl.complete( tmplItems ); + return ret; + }; + }); + + jQuery.fn.extend({ + // Use first wrapped element as template markup. + // Return wrapped set of template items, obtained by rendering template against data. + tmpl: function( data, options, parentItem ) { + return jQuery.tmpl( this[0], data, options, parentItem ); + }, + + // Find which rendered template item the first wrapped DOM element belongs to + tmplItem: function() { + return jQuery.tmplItem( this[0] ); + }, + + // Consider the first wrapped element as a template declaration, and get the compiled template or store it as a named template. + templates: function( name ) { + return jQuery.templates( name, this[0] ); + }, + + domManip: function( args, table, callback, options ) { + // This appears to be a bug in the appendTo, etc. implementation + // it should be doing .call() instead of .apply(). See #6227 + if ( args[0].nodeType ) { + var dmArgs = jQuery.makeArray( arguments ), argsLength = args.length, i = 0, tmplItem; + while ( i < argsLength && !(tmplItem = jQuery.data( args[i++], "tmplItem" ))) {}; + if ( argsLength > 1 ) { + dmArgs[0] = [jQuery.makeArray( args )]; + } + if ( tmplItem && cloneIndex ) { + dmArgs[2] = function( fragClone ) { + // Handler called by oldManip when rendered template has been inserted into DOM. + jQuery.tmpl.afterManip( this, fragClone, callback ); + } + } + oldManip.apply( this, dmArgs ); + } else { + oldManip.apply( this, arguments ); + } + cloneIndex = 0; + if ( !appendToTmplItems ) { + jQuery.tmpl.complete( newTmplItems ); + } + return this; + } + }); + + jQuery.extend({ + // Return wrapped set of template items, obtained by rendering template against data. + tmpl: function( tmpl, data, options, parentItem ) { + var ret, topLevel = !parentItem; + if ( topLevel ) { + // This is a top-level tmpl call (not from a nested template using {{tmpl}}) + parentItem = topTmplItem; + tmpl = jQuery.templates[tmpl] || jQuery.templates( null, tmpl ); + } else if ( !tmpl ) { + // The template item is already associated with DOM - this is a refresh. + // Re-evaluate rendered template for the parentItem + tmpl = parentItem.tmpl; + newTmplItems[parentItem.key] = parentItem; + parentItem.nodes = []; + updateWrapped( parentItem ); + // Rebuild, without creating a new template item + return jQuery( build( parentItem, null, parentItem.tmpl( jQuery, parentItem ) )); + } + if ( !tmpl ) { + return []; // Could throw... + } + if ( typeof data === "function" ) { + data = data.call( parentItem || {} ); + } + if ( options && options.wrapped ) { + // Create template item for wrapped content, without rendering template + parentItem = newTmplItem( options, parentItem, null, data ); + parentItem.key = ++itemKey; + wrappedItems[itemKey] = parentItem; + parentItem.tmpl = tmpl; + updateWrapped( parentItem ); + } + ret = jQuery.isArray( data ) ? + jQuery.map( data, function( dataItem ) { + return dataItem ? newTmplItem( options, parentItem, tmpl, dataItem ) : null; + }) : + [ newTmplItem( options, parentItem, tmpl, data ) ]; + + return topLevel ? jQuery( build( parentItem, null, ret ) ) : ret; + }, + + // Return rendered template item for an element. + tmplItem: function( elem ) { + var tmplItem; + if ( elem instanceof jQuery ) { + elem = tmpl[0]; + } + while ( elem && elem.nodeType === 1 && !(tmplItem = jQuery.data( elem, "tmplItem" )) && (elem = elem.parentNode) ) {} + return tmplItem || topTmplItem; + }, + + // Set: + // Use $.templates( name, tmpl ) to cache a named template, + // where tmpl is a template string, a script element or a jQuery instance wrapping a script element, etc. + // Use $( "selector" ).templates( name ) to provide access by name to a script block template declaration. + + // Get: + // Use $.templates( name ) to access a cached template. + // Also $( selectorToScriptBlock ).templates(), or $.templates( null, templateString ) + // will return the compiled template, without adding a name reference. + // If templateString includes at least one HTML tag, $.templates( templateString ) is equivalent + // to $.templates( null, templateString ) + templates: function( name, tmpl ) { + if (tmpl) { + // Compile template and associate with name + if ( typeof tmpl === "string" ) { + // This is an HTML string being passed directly in. + tmpl = buildTmplFn( tmpl ) + } else if ( tmpl instanceof jQuery ) { + tmpl = tmpl[0] || {}; + } + if ( tmpl.nodeType ) { + // If this is a template block, use cached copy, or generate tmpl function and cache. + tmpl = jQuery.data( tmpl, "tmpl" ) || jQuery.data( tmpl, "tmpl", buildTmplFn( tmpl.innerHTML )); + } + return typeof name === "string" ? (jQuery.templates[name] = tmpl) : tmpl; + } + // Return named compiled template + return typeof name !== "string" ? jQuery.templates( null, name ): + (jQuery.templates[name] || + // If not in map, treat as a selector. (If integrated with core, use quickExpr.exec) + jQuery.templates( null, htmlExpr.test( name ) ? name : jQuery( name ))); + }, + + encode: function( text ) { + // Do HTML encoding replacing < > & and ' and " by corresponding entities. + return ("" + text).split("<").join("<").split(">").join(">").split('"').join(""").split("'").join("'"); + } + }); + + jQuery.extend( jQuery.tmpl, { + tags: { + "tmpl": { + _default: { $2: "null" }, + open: "if($notnull_1){_=_.concat($item.nest($1,$2));}" + // tmpl target parameter can be of type function, so use $1, not $1a (so not auto detection of functions) + // This means that {{tmpl foo}} treats foo as a template (which IS a function). + // Explicit parens can be used if foo is a function that returns a template: {{tmpl foo()}}. + }, + "wrap": { + _default: { $2: "null" }, + open: "$item.calls(_,$1,$2);_=[];", + close: "call=$item.calls();_=call._.concat($item.wrap(call,_));" + }, + "each": { + _default: { $2: "$index, $value" }, + open: "if($notnull_1){$.each($1a,function($2){with(this){", + close: "}});}" + }, + "if": { + open: "if(($notnull_1) && $1a){", + close: "}" + }, + "else": { + open: "}else{" + }, + "html": { + open: "if($notnull_1){_.push($1a);}" + }, + "=": { + _default: { $1: "$data" }, + open: "if($notnull_1){_.push($.encode($1a));}" + } + }, + + // This stub can be overridden, e.g. in jquery.tmplPlus for providing rendered events + complete: function( items ) { + newTmplItems = {}; + }, + + // Call this from code which overrides domManip, or equivalent + // Manage cloning/storing template items etc. + afterManip: function afterManip( elem, fragClone, callback ) { + // Provides cloned fragment ready for fixup prior to and after insertion into DOM + var content = fragClone.nodeType === 11 ? + jQuery.makeArray(fragClone.childNodes) : + fragClone.nodeType === 1 ? [fragClone] : []; + + // Return fragment to original caller (e.g. append) for DOM insertion + callback.call( elem, fragClone ); + + // Fragment has been inserted:- Add inserted nodes to tmplItem data structure. Replace inserted element annotations by jQuery.data. + storeTmplItems( content ); + cloneIndex++; + } + }); + + //========================== Private helper functions, used by code above ========================== + + function build( tmplItem, nested, content ) { + // Convert hierarchical content into flat string array + // and finally return array of fragments ready for DOM insertion + var frag, ret = jQuery.map( content, function( item ) { + return (typeof item === "string") ? + // Insert template item annotations, to be converted to jQuery.data( "tmplItem" ) when elems are inserted into DOM. + item.replace( /(<\w+)(?=[\s>])(?![^>]*_tmplitem)([^>]*)/g, "$1 " + tmplItmAtt + "=\"" + tmplItem.key + "\" $2" ) : + // This is a child template item. Build nested template. + build( item, tmplItem, item._ctnt ); + }); + if ( nested ) { + return ret; + } + // top-level template + ret = ret.join(""); + + // Support templates which have initial or final text nodes, or consist only of text + // Also support HTML entities within the HTML markup. + ret.replace( /^\s*([^<\s][^<]*)?(<[\w\W]+>)([^>]*[^>\s])?\s*$/, function( all, before, middle, after) { + frag = jQuery( middle ).get(); + + storeTmplItems( frag ); + if ( before ) { + frag = unencode( before ).concat(frag); + } + if ( after ) { + frag = frag.concat(unencode( after )); + } + }); + return frag ? frag : unencode( ret ); + } + + function unencode( text ) { + // Use createElement, since createTextNode will not render HTML entities correctly + var el = document.createElement( "div" ); + el.innerHTML = text; + return jQuery.makeArray(el.childNodes); + } + + // Generate a reusable function that will serve to render a template against data + function buildTmplFn( markup ) { + return new Function("jQuery","$item", + "var $=jQuery,_=[],$data=$item.data;" + + + // Introduce the data as local variables using with(){} + "with($data){_.push('" + + + // Convert the template into pure JavaScript + $.trim(markup) + .replace( /([\\'])/g, "\\$1" ) + .replace( /[\r\t\n]/g, " " ) + .replace( /\${([^}]*)}/g, "{{= $1}}" ) + .replace( /{{(\/?)(\w+|.)(?:\(((?:.(?!}}))*?)?\))?(?:\s+(.*?)?)?(\((.*?)\))?\s*}}/g, + function( all, slash, type, fnargs, target, parens, args ) { + var cmd = jQuery.tmpl.tags[ type ], def, expr, exprAutoFnDetect; + if ( !cmd ) { + throw "Template command not found: " + type; + } + def = cmd._default || []; + if ( target ) { + target = unescape( target ); + args = args ? ("," + unescape( args ) + ")") : (parens ? ")" : ""); + if ( parens && target.indexOf(".") > -1 ) { + // Support for target being things like a.toLowerCase(); + // In that case don't call with template item as 'this' pointer. Just evaluate... + target += parens; + args = ""; + } + expr = args ? ("(" + target + ").call($item" + args) : target; + exprAutoFnDetect = args ? expr : "(typeof(" + target + ")==='function'?(" + target + ").call($item):(" + target + "))"; + } else { + expr = def["$1"] || "null"; + } + fnargs = unescape( fnargs ); + return "');" + + cmd[ slash ? "close" : "open" ] + .split( "$notnull_1" ).join( "typeof(" + target + ")!=='undefined' && (" + target + ")!=null" ) + .split( "$1a" ).join( exprAutoFnDetect ) + .split( "$1" ).join( expr ) + .split( "$2" ).join( fnargs ? + fnargs.replace( /\s*([^\(]+)\s*(\((.*?)\))?/g, function( all, name, parens, params ) { + params = params ? ("," + params + ")") : (parens ? ")" : ""); + return params ? ("(" + name + ").call($item" + params) : all; + }) + : (def["$2"]||"") + ) + + "_.push('"; + }) + + "');}return _;" + ); + } + + function updateWrapped( tmplItem ) { + if ( tmplItem.wrapped ) { + var wrapped = tmplItem.wrapped; + // Build the wrapped content + tmplItem._wrap = build( tmplItem, true, + jQuery.isArray( wrapped ) ? wrapped : [htmlExpr.test( wrapped ) ? wrapped : jQuery( wrapped ).html()] + ).join(""); + } + } + + function unescape( args ) { + return args ? args.replace( /\\'/g, "'").replace(/\\\\/g, "\\" ) : null; + } + function outerHtml( elem ) { + var div = document.createElement("div"); + div.appendChild( elem.cloneNode(true) ); + return div.innerHTML; + } + + // Store template items in jQuery.data(), ensuring a unique tmplItem data data structure for each rendered template instance. + function storeTmplItems( content ) { + var keySuffix = "_" + cloneIndex, elem, elems, newClonedItems = {}; + for ( var i = 0, l = content.length; i < l; i++ ) { + if ( (elem = content[i]).nodeType !== 1 ) { + continue; + } + elems = elem.getElementsByTagName("*"); + for ( var m = elems.length - 1; m >= 0; m-- ) { + processItemKey( elems[m] ); + } + processItemKey( elem ); + } + // Cannot remove temporary wrappedItem objects, since needed during updating of nested items. //wrappedItems = {}; + // TODO - ensure no memory leaks + + function processItemKey( el ) { + var pntKey, pntNode = el, pntItem, tmplItem, key; + // Ensure that each rendered template inserted into the DOM has its own template item, + if ( key = el.getAttribute( tmplItmAtt )) { + while ((pntNode = pntNode.parentNode).nodeType === 1 && !(pntKey = pntNode.getAttribute( tmplItmAtt ))) { } + if ( pntKey !== key ) { + // The next ancestor with a _tmplitem expando is on a different key than this one. + // So this is a top-level element within this template item + pntNode = pntNode.nodeType === 11 ? 0 : (pntNode.getAttribute( tmplItmAtt ) || 0); + if ( !(tmplItem = newTmplItems[key]) ) { + // The item is for wrapped content, and was copied from the temporary parent wrappedItem. + tmplItem = wrappedItems[key]; + tmplItem = newTmplItem( tmplItem, newTmplItems[pntNode]||wrappedItems[pntNode], null, true ); + tmplItem.key = ++itemKey; + // Note that there is a remaining issue on parenting of wrappedItems. + // ...Currently there may be additional newTmplItems items wrapped contexts, leading to duplicate rendered events. + newTmplItems[itemKey] = tmplItem; + } + if ( cloneIndex ) { + cloneTmplItem( key ); + } + } + el.removeAttribute( tmplItmAtt ); + } else if ( cloneIndex && (tmplItem = jQuery.data( el, "tmplItem" )) ) { + // This was a rendered element, cloned during append or appendTo etc. + // TmplItem stored in jQuery data has already been cloned in cloneCopyEvent. We must replace it with a fresh cloned tmplItem. + cloneTmplItem( tmplItem.key ); + newTmplItems[tmplItem.key] = tmplItem; + pntNode = jQuery.data( el.parentNode, "tmplItem" ); + pntNode = pntNode ? pntNode.key : 0; + } + if ( tmplItem ) { + pntItem = tmplItem; + // Find the template item of the parent element + while ( pntItem && pntItem.key != pntNode ) { + // Add this element as a top-level node for this rendered template item, as well as for any + // ancestor items between this item and the item of its parent element + pntItem.nodes.push( el ); + pntItem = pntItem.parent; + } + // Delete content built during rendering - reduce API surface area and memory use, and avoid exposing of stale data after rendering... + delete tmplItem._ctnt; + delete tmplItem._wrap; + // Store template item as jQuery data on the element + jQuery.data( el, "tmplItem", tmplItem ); + } + function cloneTmplItem( key ) { + key = key + keySuffix; + tmplItem = newClonedItems[key] + = (newClonedItems[key] || newTmplItem( tmplItem, newTmplItems[tmplItem.parent.key + keySuffix] || tmplItem.parent, null, true )); + } + } + } + + //---- Helper functions for template item ---- + + function tiCalls( content, tmpl, data, options ) { + if ( !content ) { + return stack.pop(); + } + var l = stack.length; + stack.push({ _: content, tmpl: tmpl, parent: l ? stack[l - 1].item : this, item:this, data: data, options: options }); + } + + function tiNest( tmpl, data, options ) { + // nested template, using {{tmpl}} tag + return jQuery.tmpl( jQuery.templates( tmpl ), data, options, this ); + } + + function tiWrap( call, wrapped ) { + // nested template, using {{wrap}} tag + var options = call.options; + options.wrapped = wrapped; + // Apply the template, which may incorporate wrapped content, + return jQuery.tmpl( jQuery.templates( call.tmpl ), call.data, options, call.parent ); + } + + function tiHtml( filter, textOnly ) { + var wrapped = this._wrap; + return jQuery.map( + jQuery( jQuery.isArray( wrapped ) ? wrapped.join("") : wrapped ).filter( filter || "*" ), + function(e) { + return textOnly ? + e.innerText || e.textContent : + e.outerHTML || outerHtml(e); + }); + } + + function tiUpdate() { + var coll = this.nodes; + jQuery.tmpl( null, null, null, this).insertBefore( coll[0] ); + jQuery( coll ).remove(); + } +})(jQuery); diff --git a/web/scripts/jquery.vgrid.0.1.5.js b/web/scripts/jquery.vgrid.0.1.5.js new file mode 100644 index 0000000..d048205 --- /dev/null +++ b/web/scripts/jquery.vgrid.0.1.5.js @@ -0,0 +1,334 @@ +/** + * jQuery VGrid v0.1.5 - variable grid layout plugin + * + * Terms of Use - jQuery VGrid + * under the MIT (http://www.opensource.org/licenses/mit-license.php) License. + * + * Copyright 2009-2010 xlune.com All rights reserved. + * (http://blog.xlune.com/2009/09/jqueryvgrid.html) + */ +(function($) +{ + function makePos(self) + { + var _childs = self.data("_vgchild"); + var _width = self.width(); + var _matrix = [[0,_width,0]]; + var _hmax=0, _c, _size, _point; + _childs.each(function(i) + { + _c = $(this); + _size = getSize(_c); + _point = getAttachPoint(_matrix, _size[0]); + _matrix = updateAttachArea(_matrix, _point, _size); + _hmax = Math.max(_hmax, _point[1] + _size[1]); + _c.data("_vgleft", _point[0]); + _c.data("_vgtop", _point[1]); + }); + self.data("_vgwrapheight", _hmax); + heightTo(self); + }; + function getAttachPoint(mtx, width) + { + var _mtx = mtx.concat().sort(matrixSortDepth); + var _max = _mtx[_mtx.length-1][2]; + for(var i=0,imax=_mtx.length; i= _max) break; + if(_mtx[i][1]-_mtx[i][0] >= width) + { + return [_mtx[i][0], _mtx[i][2]]; + } + } + return [0, _max]; + }; + function updateAttachArea(mtx, point, size) + { + var _mtx = mtx.concat().sort(matrixSortDepth); + var _cell = [point[0], point[0]+size[0], point[1]+size[1]]; + for(var i=0,imax=_mtx.length; i b[0]) || a[2] > b[2]) ? 1 : -1; + }; + function matrixSortX(a, b) + { + if(!a || !b) return 0; + return (a[0] > b[0]) ? 1 : -1; + }; + function matrixJoin(mtx, cell) + { + var _mtx = mtx.concat([cell]).sort(matrixSortX); + var _mtx_join = []; + for(var i=0,imax=_mtx.length; i 0 + && _mtx_join[_mtx_join.length-1][1] == _mtx[i][0] + && _mtx_join[_mtx_join.length-1][2] == _mtx[i][2]) + { + _mtx_join[_mtx_join.length-1][1] = _mtx[i][1]; + } + else + { + _mtx_join.push(_mtx[i]); + } + } + return _mtx_join; + }; + function matrixTrimWidth(a, b) + { + if(a[0] >= b[0] && a[0] < b[1] || a[1] >= b[0] && a[1] < b[1]) + { + if(a[0] >= b[0] && a[0] < b[1]) + { + a[0] = b[1]; + } + else + { + a[1] = b[0]; + } + } + return a; + }; + function getSize(child) + { + var _w = child.width(); + var _h = child.height(); + _w += Number(child.css("margin-left").replace('px', '')) + +Number(child.css("padding-left").replace('px', '')) + +Number(child.get(0).style.borderLeftWidth.replace('px', '')) + +Number(child.css("margin-right").replace('px', '')) + +Number(child.css("padding-right").replace('px', '')) + +Number(child.get(0).style.borderRightWidth.replace('px', '')); + _h += Number(child.css("margin-top").replace('px', '')) + +Number(child.css("padding-top").replace('px', '')) + +Number(child.get(0).style.borderTopWidth.replace('px', '')) + +Number(child.css("margin-bottom").replace('px', '')) + +Number(child.css("padding-bottom").replace('px', '')) + +Number(child.get(0).style.borderBottomWidth.replace('px', '')); + return [_w, _h]; + }; + function heightTo(self) + { + var _self = self; + var _delay = _self.data("_vgchild").length + * (_self.data("_vgopt").delay || 0) + + _self.data("_vgopt").time || 500; + _self.stop(); + if(_self.height() < _self.data("_vgwrapheight")) + { + if($.browser.msie) + { + _self.height(_self.data("_vgwrapheight")); + } + else + { + _self.animate( + { + height: _self.data("_vgwrapheight")+"px" + }, + (_self.data("_vgopt").time || 500), + "easeOutQuart" + ); + } + } + else + { + clearTimeout(_self.data("_vgwraptimeout")); + _self.data("_vgwraptimeout", setTimeout(function(){ + if($.browser.msie) + { + _self.height(_self.data("_vgwrapheight")); + } + else + { + _self.animate( + { + height: _self.data("_vgwrapheight")+"px" + }, + (_self.data("_vgopt").time || 500), + "easeOutQuart" + ); + } + }, _delay)); + } + }; + function moveTo(childs) + { + var _c; + childs.each(function(i) + { + _c = $(this); + _c.css("left", ~~_c.data("_vgleft")+"px"); + _c.css("top", ~~_c.data("_vgtop")+"px"); + }); + }; + function animateTo(childs, easing, time, delay) + { + var _self = $(childs).parent(); + var isMove = false; + var imax = childs.length; + var i,_c,_pos; + for(i=0; i") + .text(" ") + .attr("id", "_vgridspan") + .hide() + .appendTo("body"); + s.data("size", s.css("font-size")); + s.data("timer", setInterval(function(){ + if(s.css("font-size") != s.data("size")) + { + s.data("size", s.css("font-size")); + $(window).resize(); + } + }, 1000)); + }; + $.fn.extend({ + vgrid: function(option) + { + var _self = $(this); + var _opt = option || {}; + _self.data("_vgopt", _opt); + _self.data("_vgchild", _self.find("> *")); + _self.data("_vgdefchild", _self.data("_vgchild")); + _self.css({ + "position": "relative", + "width": "auto" + }); + _self.data("_vgchild").css("position", "absolute"); + makePos(_self); + moveTo(_self.data("_vgchild")); + if(_self.data("_vgopt").fadeIn) + { + var _prop = (typeof(_self.data("_vgopt").fadeIn)=='object') + ? _self.data("_vgopt").fadeIn + : {time: _self.data("_vgopt").fadeIn} ; + _self.data("_vgchild").each(function(i) + { + var _c = $(this); + _c.css('display', 'none'); + setTimeout(function(){ + _c.fadeIn(_prop.time || 250); + }, i * (_prop.delay || 0)); + }); + } + $(window).resize(function(e) + { + refleshHandler(_self); + }); + setFontSizeListener(); + return _self; + }, + vgrefresh: function(easeing, time, delay, func) + { + var _obj = $(this); + if(_obj.data("_vgchild")) + { + _obj.data("_vgchild", _obj.find("> *")); + _obj.data("_vgchild").css("position", "absolute"); + makePos(_obj); + time = typeof(time)=="number" ? time : _obj.data("_vgopt").time || 500; + delay = typeof(delay)=="number" ? delay : _obj.data("_vgopt").delay || 0; + animateTo( + _obj.data("_vgchild"), + easeing || _obj.data("_vgopt").easeing || "linear", + time, + delay + ); + if(typeof(func)=='function') + { + setTimeout( + func, + _obj.data("_vgchild").length * delay + time + ); + } + } + return _obj; + }, + vgsort: function(func, easeing, time, delay) + { + var _obj = $(this); + if(_obj.data("_vgchild")) + { + _obj.data("_vgchild", _obj.data("_vgchild").sort(func)); + _obj.data("_vgchild").each(function(num){ + $(this).appendTo(_obj); + }); + makePos(_obj); + animateTo( + _obj.data("_vgchild"), + easeing || _obj.data("_vgopt").easeing || "linear", + typeof(time)=="number" ? time : _obj.data("_vgopt").time || 500, + typeof(delay)=="number" ? delay : _obj.data("_vgopt").delay || 0 + ); + } + return _obj; + } + }); +})(jQuery); diff --git a/web/scripts/json2.js b/web/scripts/json2.js new file mode 100644 index 0000000..d552778 --- /dev/null +++ b/web/scripts/json2.js @@ -0,0 +1,7 @@ +if(!this.JSON)this.JSON={}; +(function(){function k(a){return a<10?"0"+a:a}function n(a){o.lastIndex=0;return o.test(a)?'"'+a.replace(o,function(c){var d=q[c];return typeof d==="string"?d:"\\u"+("0000"+c.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+a+'"'}function l(a,c){var d,f,i=g,e,b=c[a];if(b&&typeof b==="object"&&typeof b.toJSON==="function")b=b.toJSON(a);if(typeof j==="function")b=j.call(c,a,b);switch(typeof b){case "string":return n(b);case "number":return isFinite(b)?String(b):"null";case "boolean":case "null":return String(b); +case "object":if(!b)return"null";g+=m;e=[];if(Object.prototype.toString.apply(b)==="[object Array]"){f=b.length;for(a=0;a 16) bkey = binl_md5(bkey, key.length * 8); + + var ipad = Array(16), opad = Array(16); + for(var i = 0; i < 16; i++) + { + ipad[i] = bkey[i] ^ 0x36363636; + opad[i] = bkey[i] ^ 0x5C5C5C5C; + } + + var hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8); + return binl2rstr(binl_md5(opad.concat(hash), 512 + 128)); +} + +/* + * Convert a raw string to a hex string + */ +function rstr2hex(input) +{ + try { hexcase } catch(e) { hexcase=0; } + var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; + var output = ""; + var x; + for(var i = 0; i < input.length; i++) + { + x = input.charCodeAt(i); + output += hex_tab.charAt((x >>> 4) & 0x0F) + + hex_tab.charAt( x & 0x0F); + } + return output; +} + +/* + * Convert a raw string to a base-64 string + */ +function rstr2b64(input) +{ + try { b64pad } catch(e) { b64pad=''; } + var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var output = ""; + var len = input.length; + for(var i = 0; i < len; i += 3) + { + var triplet = (input.charCodeAt(i) << 16) + | (i + 1 < len ? input.charCodeAt(i+1) << 8 : 0) + | (i + 2 < len ? input.charCodeAt(i+2) : 0); + for(var j = 0; j < 4; j++) + { + if(i * 8 + j * 6 > input.length * 8) output += b64pad; + else output += tab.charAt((triplet >>> 6*(3-j)) & 0x3F); + } + } + return output; +} + +/* + * Convert a raw string to an arbitrary string encoding + */ +function rstr2any(input, encoding) +{ + var divisor = encoding.length; + var i, j, q, x, quotient; + + /* Convert to an array of 16-bit big-endian values, forming the dividend */ + var dividend = Array(Math.ceil(input.length / 2)); + for(i = 0; i < dividend.length; i++) + { + dividend[i] = (input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1); + } + + /* + * Repeatedly perform a long division. The binary array forms the dividend, + * the length of the encoding is the divisor. Once computed, the quotient + * forms the dividend for the next step. All remainders are stored for later + * use. + */ + var full_length = Math.ceil(input.length * 8 / + (Math.log(encoding.length) / Math.log(2))); + var remainders = Array(full_length); + for(j = 0; j < full_length; j++) + { + quotient = Array(); + x = 0; + for(i = 0; i < dividend.length; i++) + { + x = (x << 16) + dividend[i]; + q = Math.floor(x / divisor); + x -= q * divisor; + if(quotient.length > 0 || q > 0) + quotient[quotient.length] = q; + } + remainders[j] = x; + dividend = quotient; + } + + /* Convert the remainders to the output string */ + var output = ""; + for(i = remainders.length - 1; i >= 0; i--) + output += encoding.charAt(remainders[i]); + + return output; +} + +/* + * Encode a string as utf-8. + * For efficiency, this assumes the input is valid utf-16. + */ +function str2rstr_utf8(input) +{ + var output = ""; + var i = -1; + var x, y; + + while(++i < input.length) + { + /* Decode utf-16 surrogate pairs */ + x = input.charCodeAt(i); + y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0; + if(0xD800 <= x && x <= 0xDBFF && 0xDC00 <= y && y <= 0xDFFF) + { + x = 0x10000 + ((x & 0x03FF) << 10) + (y & 0x03FF); + i++; + } + + /* Encode output as utf-8 */ + if(x <= 0x7F) + output += String.fromCharCode(x); + else if(x <= 0x7FF) + output += String.fromCharCode(0xC0 | ((x >>> 6 ) & 0x1F), + 0x80 | ( x & 0x3F)); + else if(x <= 0xFFFF) + output += String.fromCharCode(0xE0 | ((x >>> 12) & 0x0F), + 0x80 | ((x >>> 6 ) & 0x3F), + 0x80 | ( x & 0x3F)); + else if(x <= 0x1FFFFF) + output += String.fromCharCode(0xF0 | ((x >>> 18) & 0x07), + 0x80 | ((x >>> 12) & 0x3F), + 0x80 | ((x >>> 6 ) & 0x3F), + 0x80 | ( x & 0x3F)); + } + return output; +} + +/* + * Encode a string as utf-16 + */ +function str2rstr_utf16le(input) +{ + var output = ""; + for(var i = 0; i < input.length; i++) + output += String.fromCharCode( input.charCodeAt(i) & 0xFF, + (input.charCodeAt(i) >>> 8) & 0xFF); + return output; +} + +function str2rstr_utf16be(input) +{ + var output = ""; + for(var i = 0; i < input.length; i++) + output += String.fromCharCode((input.charCodeAt(i) >>> 8) & 0xFF, + input.charCodeAt(i) & 0xFF); + return output; +} + +/* + * Convert a raw string to an array of little-endian words + * Characters >255 have their high-byte silently ignored. + */ +function rstr2binl(input) +{ + var output = Array(input.length >> 2); + for(var i = 0; i < output.length; i++) + output[i] = 0; + for(var i = 0; i < input.length * 8; i += 8) + output[i>>5] |= (input.charCodeAt(i / 8) & 0xFF) << (i%32); + return output; +} + +/* + * Convert an array of little-endian words to a string + */ +function binl2rstr(input) +{ + var output = ""; + for(var i = 0; i < input.length * 32; i += 8) + output += String.fromCharCode((input[i>>5] >>> (i % 32)) & 0xFF); + return output; +} + +/* + * Calculate the MD5 of an array of little-endian words, and a bit length. + */ +function binl_md5(x, len) +{ + /* append padding */ + x[len >> 5] |= 0x80 << ((len) % 32); + x[(((len + 64) >>> 9) << 4) + 14] = len; + + var a = 1732584193; + var b = -271733879; + var c = -1732584194; + var d = 271733878; + + for(var i = 0; i < x.length; i += 16) + { + var olda = a; + var oldb = b; + var oldc = c; + var oldd = d; + + a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936); + d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586); + c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819); + b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330); + a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897); + d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426); + c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341); + b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983); + a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416); + d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417); + c = md5_ff(c, d, a, b, x[i+10], 17, -42063); + b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162); + a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682); + d = md5_ff(d, a, b, c, x[i+13], 12, -40341101); + c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290); + b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329); + + a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510); + d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632); + c = md5_gg(c, d, a, b, x[i+11], 14, 643717713); + b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302); + a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691); + d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083); + c = md5_gg(c, d, a, b, x[i+15], 14, -660478335); + b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848); + a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438); + d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690); + c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961); + b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501); + a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467); + d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784); + c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473); + b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734); + + a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558); + d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463); + c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562); + b = md5_hh(b, c, d, a, x[i+14], 23, -35309556); + a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060); + d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353); + c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632); + b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640); + a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174); + d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222); + c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979); + b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189); + a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487); + d = md5_hh(d, a, b, c, x[i+12], 11, -421815835); + c = md5_hh(c, d, a, b, x[i+15], 16, 530742520); + b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651); + + a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844); + d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415); + c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905); + b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055); + a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571); + d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606); + c = md5_ii(c, d, a, b, x[i+10], 15, -1051523); + b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799); + a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359); + d = md5_ii(d, a, b, c, x[i+15], 10, -30611744); + c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380); + b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649); + a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070); + d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379); + c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259); + b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551); + + a = safe_add(a, olda); + b = safe_add(b, oldb); + c = safe_add(c, oldc); + d = safe_add(d, oldd); + } + return Array(a, b, c, d); +} + +/* + * These functions implement the four basic operations the algorithm uses. + */ +function md5_cmn(q, a, b, x, s, t) +{ + return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b); +} +function md5_ff(a, b, c, d, x, s, t) +{ + return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t); +} +function md5_gg(a, b, c, d, x, s, t) +{ + return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t); +} +function md5_hh(a, b, c, d, x, s, t) +{ + return md5_cmn(b ^ c ^ d, a, b, x, s, t); +} +function md5_ii(a, b, c, d, x, s, t) +{ + return md5_cmn(c ^ (b | (~d)), a, b, x, s, t); +} + +/* + * Add integers, wrapping at 2^32. This uses 16-bit operations internally + * to work around bugs in some JS interpreters. + */ +function safe_add(x, y) +{ + var lsw = (x & 0xFFFF) + (y & 0xFFFF); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); +} + +/* + * Bitwise rotate a 32-bit number to the left. + */ +function bit_rol(num, cnt) +{ + return (num << cnt) | (num >>> (32 - cnt)); +} diff --git a/web/scripts/placeholder.js b/web/scripts/placeholder.js new file mode 100644 index 0000000..5dff056 --- /dev/null +++ b/web/scripts/placeholder.js @@ -0,0 +1,104 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (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.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Raindrop. + * + * The Initial Developer of the Original Code is + * Mozilla Messaging, Inc.. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * */ + +/*jslint nomen: false, plusplus: false */ +/*global require: false, clearTimeout: false, setTimeout: false */ +"use strict"; + +require.def("placeholder", ["jquery"], function ($) { + + /** + * Set the input value to use placeholder value if HTML5 placeholder + * attribute is not supported. + * @param {DOMNode} input an input element. + */ + function setPlaceholder(input) { + //If no native support for placeholder then JS to the rescue! + var missingNative = !("placeholder" in input), + placeholder = input.getAttribute("placeholder"), + trimmed = input.value.trim(); + + if (!trimmed || trimmed === placeholder) { + $(input).addClass("placeholder"); + if (missingNative) { + input.value = placeholder; + if (placeholder === "password" && input.type === "password") { + input.type = "text"; + } + } + } else { + $(input).removeClass("placeholder"); + } + } + + /** + * Handles focus events on the node to see if placehoder needs to be removed. + * @param {Event} evt + */ + function onfocus(evt) { + //Clear out placeholder, change the style. + var input = evt.target, + placeholder = input.getAttribute("placeholder"); + if (input.value === placeholder) { + if (!("placeholder" in input)) { + input.value = ""; + if (placeholder === "password" && input.type === "text") { + input.type = "password"; + } + } + $(input).removeClass("placeholder"); + } + } + + /** Handles blur events on the node to see if placeholder needs to be reinstated. + * @param {Event} evt + */ + function onblur(evt) { + //Reset placeholder text if necessary. + setPlaceholder(evt.target); + } + + /** + * Scans domNode and its children for text input/textarea elements that have a placeholder + * attribute, and attach placeholder behavior to it. + * Allow for the existence of browsers that already have placeholder support + * built in. + * + * @param {DOMNode} domNode + * @param {Widget} [widget] an optional widget that will track the connect handles. + * + */ + return function (domNode, widget) { + $('input[type="text"], input[type="password"], textarea', domNode).each(function (i, node) { + //Skip nodes that have already been bound + if (node.getAttribute("data-rdwPlaceholder") !== "set") { + $(node).focus(onfocus).blur(onblur); + + node.setAttribute("data-rdwPlaceholder", "set"); + } + + //Set up initial state. + setPlaceholder(node); + }); + }; +}); diff --git a/web/scripts/rdapi.js b/web/scripts/rdapi.js new file mode 100644 index 0000000..8b6ae00 --- /dev/null +++ b/web/scripts/rdapi.js @@ -0,0 +1,221 @@ +/*jslint plusplus: false, nomen: false*/ +/*global require: false, document: false, hex_md5: false, localStorage: false, console: false */ +"use strict"; + +require.def('rdapi', + ['require', 'jquery', 'blade/object', 'blade/jig', 'friendly', 'isoDate', 'md5'], +function (require, $, object, jig, friendly, isoDate) { + + var rdapi, + userTokenHeader = 'X-Raindrop-Token', + contacts = {}, + jigFunctions = { + contact: function (identity) { + return identity.iid && identity.domain ? contacts[identity.iid] || {} : identity; + }, + contactPhotoUrl: function (contact) { + var url = 'i/face2.png', photos, mailaddr; + contact = jigFunctions.contact(contact); + photos = contact.photos; + if (photos && photos.length) { + url = photos[0].value; + photos.forEach(function (photo) { + if (photo.primary) { + url = photo.value; + } + }); + } else + if (contact.emails && contact.emails.length) { + // gravatar as a default + mailaddr = contact.emails[0].value; + contact.emails.forEach(function (email) { + if (email.primary) { + mailaddr = email.value; + } + }); + url = 'http://www.gravatar.com/avatar/' + hex_md5(mailaddr) + '?s=24 &d=identicon'; + } + return url; + }, + allMessages: function (conversation) { + return [conversation.topic].concat(conversation.messages || []); + }, + friendlyDate: function (isoString) { + return friendly.date(isoDate(isoString)).friendly; + }, + htmlBody: function (text) { + return jig.htmlEscape(text).replace(/\n/g, '
'); + } + }, + config = { + baseUrl: '/', + apiPath: 'api/', + saveTemplateData: true + }; + + //Register functions with jig + jig.addFn(jigFunctions); + + function normalize(options) { + if (typeof options === 'string') { + options = { + template: options + }; + } else if (options.templateId) { + options.template = jig.cache(options.templateId); + } + + if (!('attachData' in options)) { + options.attachData = rdapi.attachTemplateData; + } + + if (options.emptyTemplateId) { + options.emptyTemplate = jig.cache(options.emptyTemplateId); + } + + return options; + } + + function ajax(url, options) { + options.url = config.baseUrl + config.apiPath + url; + + object.mixin(options, { + limit: 30, + message_limit: 3, + dataType: 'json', + error: function (xhr, textStatus, errorThrown) { + console.log(errorThrown); + } + }); + + var oldSuccess = options.success, + userToken = localStorage[userTokenHeader]; + + //Intercept any success calls to get a hold of contacts from + //any API call that returns them. Also be sure to remember any + //user token + options.success = function (json, textStatus, xhr) { + //If user token passed back, hold on to it. + var headerToken = xhr.getResponseHeader(userTokenHeader) || ''; + if (headerToken) + localStorage[userTokenHeader] = headerToken; + + if (json.contacts) { + object.mixin(contacts, json.contacts, true); + } + if (oldSuccess) { + return oldSuccess.apply(null, arguments); + } else { + return json; + } + }; + + if (userToken) { + options.beforeSend = function (xhr) { + xhr.setRequestHeader(userTokenHeader, userToken); + }; + } + + $.ajax(options); + } + + function finishApiTemplating(html, options) { + var parentNode = options.forId ? + document.getElementById(options.forId) : null; + if (parentNode) { + parentNode.innerHTML = html; + } + + if (options.onTemplateDone) { + options.onTemplateDone(html); + } + + $(document).trigger('rdapi-done', parentNode); + } + + rdapi = function (url, options) { + options = normalize(options); + + object.mixin(options, { + success: function (json) { + var template = options.template, + emptyTemplate = options.emptyTemplate, + html = ''; + + if (options.forId && template) { + if (options.prop) { + json = jig.getObject(options.prop, json, options); + } + + if (require.isArray(json)) { + if (!json.length) { + html += jig(emptyTemplate, json, options); + } else { + json.forEach(function (item) { + html += jig(template, item, options); + }); + } + } else { + html += jig(!json ? emptyTemplate : template, json, options); + } + + finishApiTemplating(html, options); + } + }, + error: function (xhr, textStatus, errorThrown) { + if (options.emptyTemplate) { + var html = jig(options.emptyTemplate, errorThrown, options); + finishApiTemplating(html, options); + } else { + throw errorThrown; + } + } + }); + + ajax(url, options); + }; + + rdapi.contactPhotoUrl = jigFunctions.contactPhotoUrl; + + rdapi.attachTemplateData = false; + + require.ready(function () { + var apiOptions = []; + + //Build up lists of templates to use. + jig.parse({ + //rdapi adds some additional semantics to some nodes, + //to allow automatic API calls, so pull off those attributes + //to use later for each template parsed. + onBeforeParse: function (node) { + var id = node.id, + api = node.getAttribute('data-rdapi'), + forId = node.getAttribute('data-rdfor'), + prop = node.getAttribute('data-rdprop'); + + if (api) { + apiOptions.push({ + templateId: id, + api: api, + forId: forId, + prop: prop + }); + } + + //Remove the data attributes specific to rdapi + ['data-rdapi', 'data-rdprop', 'data-rdfor'].forEach(function (attr) { + node.removeAttribute(attr); + }); + } + }); + + //Finally, do all the API calls. This is a separate loop because + //all templates need to be strings before the api calls execute in + //case subtemplates are needed. + apiOptions.forEach(function (apiOption) { + rdapi(apiOption.api, apiOption); + }); + }); + + return rdapi; +}); diff --git a/web/scripts/requireplugins-jquery-1.4.2.js b/web/scripts/requireplugins-jquery-1.4.2.js new file mode 100644 index 0000000..25c89b6 --- /dev/null +++ b/web/scripts/requireplugins-jquery-1.4.2.js @@ -0,0 +1,204 @@ +/* + RequireJS Copyright (c) 2004-2010, The Dojo Foundation All Rights Reserved. + Available via the MIT, GPL or new BSD license. + see: http://github.com/jrburke/requirejs for details + RequireJS i18n Copyright (c) 2004-2010, The Dojo Foundation All Rights Reserved. + Available via the MIT, GPL or new BSD license. + see: http://github.com/jrburke/requirejs for details + RequireJS text Copyright (c) 2004-2010, The Dojo Foundation All Rights Reserved. + Available via the MIT, GPL or new BSD license. + see: http://github.com/jrburke/requirejs for details + RequireJS jsonp Copyright (c) 2004-2010, The Dojo Foundation All Rights Reserved. + Available via the MIT, GPL or new BSD license. + see: http://github.com/jrburke/requirejs for details + RequireJS order Copyright (c) 2004-2010, The Dojo Foundation All Rights Reserved. + Available via the MIT, GPL or new BSD license. + see: http://github.com/jrburke/requirejs for details + + jQuery JavaScript Library v1.4.2 + http://jquery.com/ + + Copyright 2010, John Resig + Dual licensed under the MIT or GPL Version 2 licenses. + http://jquery.org/license + + Includes Sizzle.js + http://sizzlejs.com/ + Copyright 2010, The Dojo Foundation + Released under the MIT, BSD, and GPL Licenses. + + Date: Sat Feb 13 22:33:48 2010 -0500 +*/ +var require; +(function(){function J(l){return ka.call(l)==="[object Function]"}function y(l,o,v){var z=k.plugins.defined[l];if(z)z[v.name].apply(null,v.args);else{z=k.plugins.waiting[l]||(k.plugins.waiting[l]=[]);z.push(v);o.defined.require(["require/"+l])}}function Y(l,o){var v=k.plugins.callbacks[l]=[];k.plugins[l]=function(){for(var z=0,A;A=v[z];z++)if(A.apply(null,arguments)===true&&o)return true;return false}}var B={},k,s,H=[],D,E,R,P,S,Q,U,N=/^(complete|loaded)$/,Z=!!(typeof window!=="undefined"&&navigator&& +document),c=!Z&&typeof importScripts!=="undefined",ka=Object.prototype.toString,da,w,fa;if(typeof require!=="undefined")if(J(require))return;else Q=require;w=require=function(l,o){if(typeof l==="string"&&!J(o))return require.get(l,o);return require.def.apply(require,arguments)};require.def=function(l,o,v,z){var A=null,C,W,O,X;if(typeof l==="string"){C=l.indexOf("!");if(C!==-1){O=l.substring(0,C);l=l.substring(C+1,l.length)}if(!require.isArray(o)){z=v;v=o;o=[]}z=z||k.ctxName;if((C=k.contexts[z])&& +(C.defined[l]||C.waiting[l]))return require}else if(require.isArray(l)){z=v;v=o;o=l;l=null}else if(require.isFunction(l)){v=l;z=o;l=null;o=[]}else{A=l;l=null;if(require.isFunction(o)){z=v;v=o;o=[]}z=z||A.context}z=z||k.ctxName;C=k.contexts[z];if(!C){C={contextName:z,config:{waitSeconds:7,baseUrl:k.baseUrl||"./",paths:{}},waiting:[],specified:{require:true,exports:true,module:true},loaded:{require:true},urlFetched:{},defined:{},modifiers:{}};C.defined.require=require;k.plugins.newContext&&k.plugins.newContext(C); +C=k.contexts[z]=C}if(A){if(A.baseUrl)if(A.baseUrl.charAt(A.baseUrl.length-1)!=="/")A.baseUrl+="/";X=C.config.paths;require.mixin(C.config,A,true);if(A.paths){for(W in A.paths)W in B||(X[W]=A.paths[W]);C.config.paths=X}if(A.priority){w(A.priority);C.config.priorityWait=A.priority}if(A.deps||A.callback)w(A.deps||[],A.callback);A.ready&&require.ready(A.ready);if(!o)return require}if(o){A=o;o=[];for(W=0;W0;A--){C=l.slice(0,A).join("/");if(z[C]){l.splice(0, +A,z[C]);break}}o=l.join("/")+(o||".js");return(o.charAt(0)==="/"||o.match(/^\w+:/)?"":v.baseUrl)+o}};require.checkLoaded=function(l){var o=k.contexts[l||k.ctxName],v=o.config.waitSeconds*1E3,z=v&&o.startTime+v<(new Date).getTime(),A,C;v="";var W=false,O=false,X,$,ea=k.plugins.isWaiting,ga=k.plugins.orderDeps,na={};if(!o.isCheckLoaded){if(o.config.priorityWait){C=true;for(A=0;$=o.config.priorityWait[A];A++)if(!o.loaded[$]){C=false;break}if(C){delete o.config.priorityWait;require.resume()}else return}o.isCheckLoaded= +true;C=o.waiting;A=o.loaded;for(X in A)if(!(X in B)){W=true;if(!A[X])if(z)v+=X+" ";else{O=true;break}}if(!W&&!C.length&&(!ea||!ea(o)))o.isCheckLoaded=false;else{if(z&&v){o=new Error("require.js load timeout for modules: "+v);o.requireType="timeout";o.requireModules=v;throw o;}if(O){o.isCheckLoaded=false;if(Z||c)setTimeout(function(){require.checkLoaded(l)},50)}else{o.waiting=[];o.loaded={};ga&&ga(o);for(A=0;v=C[A];A++)require.exec(v,na,C,o);o.isCheckLoaded=false;if(o.waiting.length||ea&&ea(o))require.checkLoaded(l); +else if(!H.length){k.ctxName="_";k.isDone=true;require.callReady&&require.callReady()}}}}};require.exec=function(l,o,v,z){if(l){var A=l.name,C=l.callback;C=l.deps;var W,O,X=z.defined,$,ea=[],ga=false;if(A){if(o[A]||A in X)return X[A];o[A]=true}if(C)for(W=0;O=C[W];W++){O=O.name;if(O==="exports"){O=X[A]={};ga=true}else O=O==="module"?{id:A,uri:A?require.nameToUrl(A,null,z.contextName):undefined}:O in X?X[O]:o[O]?undefined:require.exec(v[v[O]],o,v,z);ea.push(O)}if((C=l.callback)&&require.isFunction(C)){$= +require.execCb(A,C,ea);if(A)if(ga)$=X[A];else if(A in X)throw new Error(A+" has already been defined");else X[A]=$}return $}};require.execCb=function(l,o,v){return o.apply(null,v)};require.onScriptLoad=function(l){var o=l.currentTarget||l.srcElement,v;if(l.type==="load"||N.test(o.readyState)){l=o.getAttribute("data-requirecontext");v=o.getAttribute("data-requiremodule");k.contexts[l].loaded[v]=true;require.checkLoaded(l);o.removeEventListener?o.removeEventListener("load",require.onScriptLoad,false): +o.detachEvent("onreadystatechange",require.onScriptLoad)}};require.attach=function(l,o,v,z,A){var C;if(Z){z=z||require.onScriptLoad;C=document.createElement("script");C.type=A||"text/javascript";C.charset="utf-8";k.skipAsync[l]||C.setAttribute("async","async");C.setAttribute("data-requirecontext",o);C.setAttribute("data-requiremodule",v);C.addEventListener?C.addEventListener("load",z,false):C.attachEvent("onreadystatechange",z);C.src=l;return fa?k.head.insertBefore(C,fa):k.head.appendChild(C)}else if(c){o= +k.contexts[o].loaded;o[v]=false;importScripts(l);o[v]=true}return null};k.baseUrl=Q&&Q.baseUrl;if(Z&&(!k.baseUrl||!k.head)){D=document.getElementsByTagName("script");R=Q&&Q.baseUrlMatch?Q.baseUrlMatch:/(requireplugins-|require-)?jquery[\-\d\.]*(min)?\.js(\W|$)/i;for(s=D.length-1;s>-1&&(E=D[s]);s--){if(!k.head)k.head=E.parentNode;if(P=E.src)if(S=P.match(R)){k.baseUrl=P.substring(0,S.index);break}}}require.pageLoaded=function(){if(!k.isPageLoaded){k.isPageLoaded=true;da&&clearInterval(da);if(U)document.readyState= +"complete";require.callReady()}};require.callReady=function(){var l=k.readyCalls,o,v;if(k.isPageLoaded&&k.isDone&&l.length){k.readyCalls=[];for(o=0;v=l[o];o++)v()}};require.ready=function(l){k.isPageLoaded&&k.isDone?l():k.readyCalls.push(l);return require};if(Z){if(document.addEventListener){document.addEventListener("DOMContentLoaded",require.pageLoaded,false);window.addEventListener("load",require.pageLoaded,false);if(!document.readyState){U=true;document.readyState="loading"}}else if(window.attachEvent){window.attachEvent("onload", +require.pageLoaded);if(self===self.top)da=setInterval(function(){try{if(document.body){document.documentElement.doScroll("left");require.pageLoaded()}}catch(l){}},30)}document.readyState==="complete"&&require.pageLoaded()}Q&&w(Q)})(); +(function(){function J(k,s){s=s.nlsWaiting;return s[k]||(s[k]=s[s.push({_name:k})-1])}function y(k,s,H,D){var E,R,P,S,Q,U,N="root";R=H.split("-");P=[];S=J(k,D);for(E=R.length;E>-1;E--){Q=E?R.slice(0,E).join("-"):"root";if(U=s[Q]){if(H===D.config.locale&&!S._match)S._match=Q;if(N==="root")N=Q;S[Q]=Q;if(U===true){U=k.split("/");U.splice(-1,0,Q);U=U.join("/");!D.specified[U]&&!(U in D.loaded)&&!D.defined[U]&&P.push(U)}}}if(N!==H)if(D.defined[N])D.defined[H]=D.defined[N];else S[H]=N;P.length&&D.defined.require(P)} +var Y=/(^.*(^|\/)nls(\/|$))([^\/]*)\/?([^\/]*)/,B={};require.plugin({prefix:"i18n",require:function(k,s,H,D){var E,R=D.defined[k];E=Y.exec(k);if(E[5]){k=E[1]+E[5];s=J(k,D);s[E[4]]=E[4];s=D.nls[k];if(!s){D.defined.require([k]);s=D.nls[k]={}}s[E[4]]=H}else{if(s=D.nls[k])require.mixin(s,R);else s=D.nls[k]=R;D.nlsRootLoaded[k]=true;if(E=D.nlsToLoad[k]){delete D.nlsToLoad[k];for(H=0;H0;H--){c=P.slice(0,H).join("-");c!=="root"&&R[c]&&require.mixin(Q,R[c])}R.root&&require.mixin(Q,R.root);k.defined[U+"/"+N+"/"+S]=Q}k.defined[D]=k.defined[U+"/"+Z+"/"+S];if(da)for(N in da)N in +B||(k.defined[U+"/"+N+"/"+S]=k.defined[U+"/"+da[N]+"/"+S])}}})})(); +(function(){var J=["Msxml2.XMLHTTP","Microsoft.XMLHTTP","Msxml2.XMLHTTP.4.0"],y=/^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im,Y=/]*>\s*([\s\S]+)\s*<\/body>/im;if(!require.textStrip)require.textStrip=function(B){if(B){B=B.replace(y,"");var k=B.match(Y);if(k)B=k[1]}else B="";return B};if(!require.getXhr)require.getXhr=function(){var B,k,s;if(typeof XMLHttpRequest!=="undefined")return new XMLHttpRequest;else for(k=0;k<3;k++){s=J[k];try{B=new ActiveXObject(s)}catch(H){}if(B){J=[s]; +break}}if(!B)throw new Error("require.getXhr(): XMLHttpRequest not available");return B};if(!require.fetchText)require.fetchText=function(B,k){var s=require.getXhr();s.open("GET",B,true);s.onreadystatechange=function(){s.readyState===4&&k(s.responseText)};s.send(null)};require.plugin({prefix:"text",require:function(){},newContext:function(B){require.mixin(B,{text:{},textWaiting:[]})},load:function(B,k){var s=false,H=null,D,E=B.indexOf("."),R=B.substring(0,E),P=B.substring(E+1,B.length),S=require.s.contexts[k], +Q=S.textWaiting;E=P.indexOf("!");if(E!==-1){s=P.substring(E+1,P.length);P=P.substring(0,E);E=s.indexOf("!");if(E!==-1&&s.substring(0,E)==="strip"){H=s.substring(E+1,s.length);s="strip"}else if(s!=="strip"){H=s;s=null}}D=R+"!"+P;E=s?D+"!"+s:D;if(H!==null&&!S.text[D])S.defined[B]=S.text[D]=H;else if(!S.text[D]&&!S.textWaiting[D]&&!S.textWaiting[E]){Q[E]||(Q[E]=Q[Q.push({name:B,key:D,fullKey:E,strip:!!s})-1]);s=require.nameToUrl(R,"."+P,k);S.loaded[B]=false;require.fetchText(s,function(U){S.text[D]= +U;S.loaded[B]=true;require.checkLoaded(k)})}},checkDeps:function(){},isWaiting:function(B){return!!B.textWaiting.length},orderDeps:function(B){var k,s,H,D=B.textWaiting;B.textWaiting=[];for(k=0;s=D[k];k++){H=B.text[s.key];B.defined[s.name]=s.strip?require.textStrip(H):H}}})})(); +(function(){var J=0;require._jsonp={};require.plugin({prefix:"jsonp",require:function(){},newContext:function(y){require.mixin(y,{jsonpWaiting:[]})},load:function(y,Y){var B=y.indexOf("?"),k=y.substring(0,B);B=y.substring(B+1,y.length);var s=require.s.contexts[Y],H={name:y},D="f"+J++,E=require.s.head,R=E.ownerDocument.createElement("script");require._jsonp[D]=function(P){H.value=P;s.loaded[y]=true;require.checkLoaded(Y);setTimeout(function(){E.removeChild(R);delete require._jsonp[D]},15)};s.jsonpWaiting.push(H); +k=require.nameToUrl(k,"?",Y);k+=(k.indexOf("?")===-1?"?":"")+B.replace("?","require._jsonp."+D);s.loaded[y]=false;R.type="text/javascript";R.charset="utf-8";R.src=k;R.setAttribute("async","async");E.appendChild(R)},checkDeps:function(){},isWaiting:function(y){return!!y.jsonpWaiting.length},orderDeps:function(y){var Y,B,k=y.jsonpWaiting;y.jsonpWaiting=[];for(Y=0;B=k[Y];Y++)y.defined[B.name]=B.value}})})(); +(function(){function J(B){var k=B.currentTarget||B.srcElement,s,H,D,E;if(B.type==="load"||Y.test(k.readyState)){H=k.getAttribute("data-requirecontext");s=k.getAttribute("data-requiremodule");B=require.s.contexts[H];D=B.orderWaiting;E=B.orderCached;E[s]=true;for(s=0;E[D[s]];s++);s>0&&require(D.splice(0,s),H);if(!D.length)B.orderCached={};setTimeout(function(){k.parentNode.removeChild(k)},15)}}var y=window.opera&&Object.prototype.toString.call(window.opera)==="[object Opera]"||"MozAppearance"in document.documentElement.style, +Y=/^(complete|loaded)$/;require.plugin({prefix:"order",require:function(){},newContext:function(B){require.mixin(B,{orderWaiting:[],orderCached:{}})},load:function(B,k){var s=require.s.contexts[k],H=require.nameToUrl(B,null,k);require.s.skipAsync[H]=true;if(y)require([B],k);else{s.orderWaiting.push(B);s.loaded[B]=false;require.attach(H,k,B,J,"script/cache")}},checkDeps:function(){},isWaiting:function(B){return!!B.orderWaiting.length},orderDeps:function(){}})})(); +(function(J,y){function Y(){if(!c.isReady){try{w.documentElement.doScroll("left")}catch(a){setTimeout(Y,1);return}c.ready()}}function B(a,b){b.src?c.ajax({url:b.src,async:false,dataType:"script"}):c.globalEval(b.text||b.textContent||b.innerHTML||"");b.parentNode&&b.parentNode.removeChild(b)}function k(a,b,d,f,e,i){var j=a.length;if(typeof b==="object"){for(var r in b)k(a,r,b[r],f,e,d);return a}if(d!==y){f=!i&&f&&c.isFunction(d);for(r=0;r)[^>]*$|^#([\w-]+)$/,o=/^.[^:#\[\.,]*$/,v=/\S/,z= +/^(\s|\u00A0)+|(\s|\u00A0)+$/g,A=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,C=navigator.userAgent,W=false,O=[],X=!!(typeof require!=="undefined"&&require.def),$,ea=Object.prototype.toString,ga=Object.prototype.hasOwnProperty,na=Array.prototype.push,pa=Array.prototype.slice,Ha=Array.prototype.indexOf;c.fn=c.prototype={init:function(a,b){var d,f;if(!a)return this;if(a.nodeType){this.context=this[0]=a;this.length=1;return this}if(a==="body"&&!b){this.context=w;this[0]=w.body;this.selector="body";this.length=1;return this}if(typeof a=== +"string")if((d=l.exec(a))&&(d[1]||!b))if(d[1]){f=b?b.ownerDocument||b:w;if(a=A.exec(a))if(c.isPlainObject(b)){a=[w.createElement(a[1])];c.fn.attr.call(a,b,true)}else a=[f.createElement(a[1])];else{a=U([d[1]],[f]);a=(a.cacheable?a.fragment.cloneNode(true):a.fragment).childNodes}return c.merge(this,a)}else{if(b=w.getElementById(d[2])){if(b.id!==d[2])return fa.find(a);this.length=1;this[0]=b}this.context=w;this.selector=a;return this}else if(!b&&/^\w+$/.test(a)){this.selector=a;this.context=w;a=w.getElementsByTagName(a); +return c.merge(this,a)}else return!b||b.jquery?(b||fa).find(a):c(b).find(a);else if(c.isFunction(a))return fa.ready(a);if(a.selector!==y){this.selector=a.selector;this.context=a.context}return c.makeArray(a,this)},selector:"",jquery:"1.4.2",length:0,size:function(){return this.length},toArray:function(){return pa.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this.slice(a)[0]:this[a]},pushStack:function(a,b,d){var f=c();c.isArray(a)?na.apply(f,a):c.merge(f,a);f.prevObject=this;f.context= +this.context;if(b==="find")f.selector=this.selector+(this.selector?" ":"")+d;else if(b)f.selector=this.selector+"."+b+"("+d+")";return f},each:function(a,b){return c.each(this,a,b)},ready:function(a){c.bindReady();if(c.isReady&&(!X||require.s.isDone))a.call(w,c);else{O||(O=[]);O.push(a)}return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(pa.apply(this,arguments),"slice", +pa.call(arguments).join(","))},map:function(a){return this.pushStack(c.map(this,function(b,d){return a.call(b,d,b)}))},end:function(){return this.prevObject||c(null)},push:na,sort:[].sort,splice:[].splice};c.fn.init.prototype=c.fn;c.extend=c.fn.extend=function(){var a=arguments[0]||{},b=1,d=arguments.length,f=false,e,i,j,r;if(typeof a==="boolean"){f=a;a=arguments[1]||{};b=2}if(typeof a!=="object"&&!c.isFunction(a))a={};if(d===b){a=this;--b}for(;b";a=w.createDocumentFragment();a.appendChild(d.firstChild);c.support.checkClone=a.cloneNode(true).cloneNode(true).lastChild.checked;c(function(){var m=w.createElement("div");m.style.width=m.style.paddingLeft="1px";w.body.appendChild(m); +c.boxModel=c.support.boxModel=m.offsetWidth===2;w.body.removeChild(m).style.display="none"});a=function(m){var q=w.createElement("div");m="on"+m;var x=m in q;if(!x){q.setAttribute(m,"return;");x=typeof q[m]==="function"}return x};c.support.submitBubbles=a("submit");c.support.changeBubbles=a("change");a=b=d=e=i=null}})();c.props={"for":"htmlFor","class":"className",readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",colspan:"colSpan",tabindex:"tabIndex",usemap:"useMap", +frameborder:"frameBorder"};var ha="jQuery"+s(),$a=0,Ia={};c.extend({cache:{},expando:ha,noData:{embed:true,object:true,applet:true},data:function(a,b,d){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==J?Ia:a;var f=a[ha],e=c.cache;if(!f&&typeof b==="string"&&d===y)return null;f||(f=++$a);if(typeof b==="object"){a[ha]=f;e[f]=c.extend(true,{},b)}else if(!e[f]){a[ha]=f;e[f]={}}a=e[f];if(d!==y)a[b]=d;return typeof b==="string"?a[b]:a}},removeData:function(a,b){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a= +a==J?Ia:a;var d=a[ha],f=c.cache,e=f[d];if(b){if(e){delete e[b];c.isEmptyObject(e)&&c.removeData(a)}}else{if(c.support.deleteExpando)delete a[c.expando];else a.removeAttribute&&a.removeAttribute(c.expando);delete f[d]}}}});c.fn.extend({data:function(a,b){if(typeof a==="undefined"&&this.length)return c.data(this[0]);else if(typeof a==="object")return this.each(function(){c.data(this,a)});var d=a.split(".");d[1]=d[1]?"."+d[1]:"";if(b===y){var f=this.triggerHandler("getData"+d[1]+"!",[d[0]]);if(f===y&& +this.length)f=c.data(this[0],a);return f===y&&d[1]?this.data(d[0]):f}else return this.trigger("setData"+d[1]+"!",[d[0],b]).each(function(){c.data(this,a,b)})},removeData:function(a){return this.each(function(){c.removeData(this,a)})}});c.extend({queue:function(a,b,d){if(a){b=(b||"fx")+"queue";var f=c.data(a,b);if(!d)return f||[];if(!f||c.isArray(d))f=c.data(a,b,c.makeArray(d));else f.push(d);return f}},dequeue:function(a,b){b=b||"fx";var d=c.queue(a,b),f=d.shift();if(f==="inprogress")f=d.shift(); +if(f){b==="fx"&&d.unshift("inprogress");f.call(a,function(){c.dequeue(a,b)})}}});c.fn.extend({queue:function(a,b){if(typeof a!=="string"){b=a;a="fx"}if(b===y)return c.queue(this[0],a);return this.each(function(){var d=c.queue(this,a,b);a==="fx"&&d[0]!=="inprogress"&&c.dequeue(this,a)})},dequeue:function(a){return this.each(function(){c.dequeue(this,a)})},delay:function(a,b){a=c.fx?c.fx.speeds[a]||a:a;b=b||"fx";return this.queue(b,function(){var d=this;setTimeout(function(){c.dequeue(d,b)},a)})},clearQueue:function(a){return this.queue(a|| +"fx",[])}});var Ja=/[\n\t]/g,ua=/\s+/,ab=/\r/g,bb=/href|src|style/,cb=/(button|input)/i,db=/(button|input|object|select|textarea)/i,eb=/^(a|area)$/i,Ka=/radio|checkbox/;c.fn.extend({attr:function(a,b){return k(this,a,b,true,c.attr)},removeAttr:function(a){return this.each(function(){c.attr(this,a,"");this.nodeType===1&&this.removeAttribute(a)})},addClass:function(a){if(c.isFunction(a))return this.each(function(q){var x=c(this);x.addClass(a.call(this,q,x.attr("class")))});if(a&&typeof a==="string")for(var b= +(a||"").split(ua),d=0,f=this.length;d-1)return true;return false},val:function(a){if(a===y){var b=this[0];if(b){if(c.nodeName(b,"option"))return(b.attributes.value||{}).specified?b.value:b.text;if(c.nodeName(b,"select")){var d=b.selectedIndex,f= +[],e=b.options;b=b.type==="select-one";if(d<0)return null;var i=b?d:0;for(d=b?d+1:e.length;i=0;else if(c.nodeName(this,"select")){var G=c.makeArray(x);c("option",this).each(function(){this.selected=c.inArray(c(this).val(),G)>=0});if(!G.length)this.selectedIndex=-1}else this.value=x}})}});c.extend({attrFn:{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true},attr:function(a,b,d,f){if(!a||a.nodeType===3||a.nodeType===8)return y;if(f&&b in c.attrFn)return c(a)[b](d);f=a.nodeType!==1||!c.isXMLDoc(a);var e=d!==y;b=f&&c.props[b]||b;if(a.nodeType=== +1){var i=bb.test(b);if(b in a&&f&&!i){if(e){b==="type"&&cb.test(a.nodeName)&&a.parentNode&&c.error("type property can't be changed");a[b]=d}if(c.nodeName(a,"form")&&a.getAttributeNode(b))return a.getAttributeNode(b).nodeValue;if(b==="tabIndex")return(b=a.getAttributeNode("tabIndex"))&&b.specified?b.value:db.test(a.nodeName)||eb.test(a.nodeName)&&a.href?0:y;return a[b]}if(!c.support.style&&f&&b==="style"){if(e)a.style.cssText=""+d;return a.style.cssText}e&&a.setAttribute(b,""+d);a=!c.support.hrefNormalized&& +f&&i?a.getAttribute(b,2):a.getAttribute(b);return a===null?y:a}return c.style(a,b,d)}});var oa=/\.(.*)$/,fb=function(a){return a.replace(/[^\w\s\.\|`]/g,function(b){return"\\"+b})};c.event={add:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){if(a.setInterval&&a!==J&&!a.frameElement)a=J;var e,i;if(d.handler){e=d;d=e.handler}if(!d.guid)d.guid=c.guid++;if(i=c.data(a)){var j=i.events=i.events||{},r=i.handle;if(!r)i.handle=r=function(){return typeof c!=="undefined"&&!c.event.triggered?c.event.handle.apply(r.elem, +arguments):y};r.elem=a;b=b.split(" ");for(var m,q=0,x;m=b[q++];){i=e?c.extend({},e):{handler:d,data:f};if(m.indexOf(".")>-1){x=m.split(".");m=x.shift();i.namespace=x.slice(0).sort().join(".")}else{x=[];i.namespace=""}i.type=m;i.guid=d.guid;var G=j[m],M=c.event.special[m]||{};if(!G){G=j[m]=[];if(!M.setup||M.setup.call(a,f,x,r)===false)if(a.addEventListener)a.addEventListener(m,r,false);else a.attachEvent&&a.attachEvent("on"+m,r)}if(M.add){M.add.call(a,i);if(!i.handler.guid)i.handler.guid=d.guid}G.push(i); +c.event.global[m]=true}a=null}}},global:{},remove:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){var e,i=0,j,r,m,q,x,G,M=c.data(a),V=M&&M.events;if(M&&V){if(b&&b.type){d=b.handler;b=b.type}if(!b||typeof b==="string"&&b.charAt(0)==="."){b=b||"";for(e in V)c.event.remove(a,e+b)}else{for(b=b.split(" ");e=b[i++];){q=e;j=e.indexOf(".")<0;r=[];if(!j){r=e.split(".");e=r.shift();m=new RegExp("(^|\\.)"+c.map(r.slice(0).sort(),fb).join("\\.(?:.*\\.)?")+"(\\.|$)")}if(x=V[e])if(d){q=c.event.special[e]|| +{};for(T=f||0;T=0){a.type=e=e.slice(0,-1);a.exclusive=true}if(!d){a.stopPropagation();c.event.global[e]&&c.each(c.cache,function(){this.events&&this.events[e]&&c.event.trigger(a,b,this.handle.elem)})}if(!d||d.nodeType===3||d.nodeType===8)return y;a.result=y;a.target=d;b=c.makeArray(b);b.unshift(a)}a.currentTarget=d;(f=c.data(d,"handle"))&&f.apply(d,b);f=d.parentNode||d.ownerDocument; +try{if(!(d&&d.nodeName&&c.noData[d.nodeName.toLowerCase()]))if(d["on"+e]&&d["on"+e].apply(d,b)===false)a.result=false}catch(i){}if(!a.isPropagationStopped()&&f)c.event.trigger(a,b,f,true);else if(!a.isDefaultPrevented()){f=a.target;var j,r=c.nodeName(f,"a")&&e==="click",m=c.event.special[e]||{};if((!m._default||m._default.call(d,a)===false)&&!r&&!(f&&f.nodeName&&c.noData[f.nodeName.toLowerCase()])){try{if(f[e]){if(j=f["on"+e])f["on"+e]=null;c.event.triggered=true;f[e]()}}catch(q){}if(j)f["on"+e]= +j;c.event.triggered=false}}},handle:function(a){var b,d,f,e;a=arguments[0]=c.event.fix(a||J.event);a.currentTarget=this;b=a.type.indexOf(".")<0&&!a.exclusive;if(!b){d=a.type.split(".");a.type=d.shift();f=new RegExp("(^|\\.)"+d.slice(0).sort().join("\\.(?:.*\\.)?")+"(\\.|$)")}e=c.data(this,"events");d=e[a.type];if(e&&d){d=d.slice(0);e=0;for(var i=d.length;e-1?c.map(a.options,function(f){return f.selected}).join("-"):"";else if(a.nodeName.toLowerCase()==="select")d=a.selectedIndex;return d},xa=function(a,b){var d=a.target,f,e;if(!(!va.test(d.nodeName)||d.readOnly)){f=c.data(d,"_change_data");e=Oa(d);if(a.type!=="focusout"||d.type!=="radio")c.data(d,"_change_data",e);if(!(f===y||e===f))if(f!=null||e){a.type="change";return c.event.trigger(a, +b,d)}}};c.event.special.change={filters:{focusout:xa,click:function(a){var b=a.target,d=b.type;if(d==="radio"||d==="checkbox"||b.nodeName.toLowerCase()==="select")return xa.call(this,a)},keydown:function(a){var b=a.target,d=b.type;if(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(d==="checkbox"||d==="radio")||d==="select-multiple")return xa.call(this,a)},beforeactivate:function(a){a=a.target;c.data(a,"_change_data",Oa(a))}},setup:function(){if(this.type==="file")return false; +for(var a in wa)c.event.add(this,a+".specialChange",wa[a]);return va.test(this.nodeName)},teardown:function(){c.event.remove(this,".specialChange");return va.test(this.nodeName)}};wa=c.event.special.change.filters}w.addEventListener&&c.each({focus:"focusin",blur:"focusout"},function(a,b){function d(f){f=c.event.fix(f);f.type=b;return c.event.handle.call(this,f)}c.event.special[b]={setup:function(){this.addEventListener(a,d,true)},teardown:function(){this.removeEventListener(a,d,true)}}});c.each(["bind", +"one"],function(a,b){c.fn[b]=function(d,f,e){if(typeof d==="object"){for(var i in d)this[b](i,f,d[i],e);return this}if(c.isFunction(f)){e=f;f=y}var j=b==="one"?c.proxy(e,function(m){c(this).unbind(m,j);return e.apply(this,arguments)}):e;if(d==="unload"&&b!=="one")this.one(d,f,e);else{i=0;for(var r=this.length;i0){L=F;break}}F=F[g]}p[u]=L}}}var f=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, +e=0,i=Object.prototype.toString,j=false,r=true;[0,0].sort(function(){r=false;return 0});var m=function(g,h,n,p){n=n||[];var u=h=h||w;if(h.nodeType!==1&&h.nodeType!==9)return[];if(!g||typeof g!=="string")return n;for(var t=[],I,F,L,qa,ia=true,la=K(h),ja=g;(f.exec(""),I=f.exec(ja))!==null;){ja=I[3];t.push(I[1]);if(I[2]){qa=I[3];break}}if(t.length>1&&x.exec(g))if(t.length===2&&q.relative[t[0]])F=ya(t[0]+t[1],h);else for(F=q.relative[t[0]]?[h]:m(t.shift(),h);t.length;){g=t.shift();if(q.relative[g])g+= +t.shift();F=ya(g,F)}else{if(!p&&t.length>1&&h.nodeType===9&&!la&&q.match.ID.test(t[0])&&!q.match.ID.test(t[t.length-1])){I=m.find(t.shift(),h,la);h=I.expr?m.filter(I.expr,I.set)[0]:I.set[0]}if(h){I=p?{expr:t.pop(),set:M(p)}:m.find(t.pop(),t.length===1&&(t[0]==="~"||t[0]==="+")&&h.parentNode?h.parentNode:h,la);F=I.expr?m.filter(I.expr,I.set):I.set;if(t.length>0)L=M(F);else ia=false;for(;t.length;){var aa=t.pop();I=aa;if(q.relative[aa])I=t.pop();else aa="";if(I==null)I=h;q.relative[aa](L,I,la)}}else L= +[]}L||(L=F);L||m.error(aa||g);if(i.call(L)==="[object Array]")if(ia)if(h&&h.nodeType===1)for(g=0;L[g]!=null;g++){if(L[g]&&(L[g]===true||L[g].nodeType===1&&ba(h,L[g])))n.push(F[g])}else for(g=0;L[g]!=null;g++)L[g]&&L[g].nodeType===1&&n.push(F[g]);else n.push.apply(n,L);else M(L,n);if(qa){m(qa,u,n,p);m.uniqueSort(n)}return n};m.uniqueSort=function(g){if(T){j=r;g.sort(T);if(j)for(var h=1;h":function(g,h){var n=typeof h==="string";if(n&&!/\W/.test(h)){h=h.toLowerCase();for(var p=0,u=g.length;p= +0))n||p.push(I);else if(n)h[t]=false;return false},ID:function(g){return g[1].replace(/\\/g,"")},TAG:function(g){return g[1].toLowerCase()},CHILD:function(g){if(g[1]==="nth"){var h=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(g[2]==="even"&&"2n"||g[2]==="odd"&&"2n+1"||!/\D/.test(g[2])&&"0n+"+g[2]||g[2]);g[2]=h[1]+(h[2]||1)-0;g[3]=h[3]-0}g[0]=e++;return g},ATTR:function(g,h,n,p,u,t){h=g[1].replace(/\\/g,"");if(!t&&q.attrMap[h])g[1]=q.attrMap[h];if(g[2]==="~=")g[4]=" "+g[4]+" ";return g},PSEUDO:function(g,h,n,p, +u){if(g[1]==="not")if((f.exec(g[3])||"").length>1||/^\w/.test(g[3]))g[3]=m(g[3],null,null,h);else{g=m.filter(g[3],h,n,true^u);n||p.push.apply(p,g);return false}else if(q.match.POS.test(g[0])||q.match.CHILD.test(g[0]))return true;return g},POS:function(g){g.unshift(true);return g}},filters:{enabled:function(g){return g.disabled===false&&g.type!=="hidden"},disabled:function(g){return g.disabled===true},checked:function(g){return g.checked===true},selected:function(g){return g.selected===true},parent:function(g){return!!g.firstChild}, +empty:function(g){return!g.firstChild},has:function(g,h,n){return!!m(n[3],g).length},header:function(g){return/h\d/i.test(g.nodeName)},text:function(g){return"text"===g.type},radio:function(g){return"radio"===g.type},checkbox:function(g){return"checkbox"===g.type},file:function(g){return"file"===g.type},password:function(g){return"password"===g.type},submit:function(g){return"submit"===g.type},image:function(g){return"image"===g.type},reset:function(g){return"reset"===g.type},button:function(g){return"button"=== +g.type||g.nodeName.toLowerCase()==="button"},input:function(g){return/input|select|textarea|button/i.test(g.nodeName)}},setFilters:{first:function(g,h){return h===0},last:function(g,h,n,p){return h===p.length-1},even:function(g,h){return h%2===0},odd:function(g,h){return h%2===1},lt:function(g,h,n){return hn[3]-0},nth:function(g,h,n){return n[3]-0===h},eq:function(g,h,n){return n[3]-0===h}},filter:{PSEUDO:function(g,h,n,p){var u=h[1],t=q.filters[u];if(t)return t(g, +n,h,p);else if(u==="contains")return(g.textContent||g.innerText||a([g])||"").indexOf(h[3])>=0;else if(u==="not"){h=h[3];n=0;for(p=h.length;n=0}},ID:function(g,h){return g.nodeType===1&&g.getAttribute("id")===h},TAG:function(g,h){return h==="*"&&g.nodeType===1||g.nodeName.toLowerCase()===h},CLASS:function(g,h){return(" "+(g.className||g.getAttribute("class"))+" ").indexOf(h)>-1},ATTR:function(g,h){var n= +h[1];g=q.attrHandle[n]?q.attrHandle[n](g):g[n]!=null?g[n]:g.getAttribute(n);n=g+"";var p=h[2];h=h[4];return g==null?p==="!=":p==="="?n===h:p==="*="?n.indexOf(h)>=0:p==="~="?(" "+n+" ").indexOf(h)>=0:!h?n&&g!==false:p==="!="?n!==h:p==="^="?n.indexOf(h)===0:p==="$="?n.substr(n.length-h.length)===h:p==="|="?n===h||n.substr(0,h.length+1)===h+"-":false},POS:function(g,h,n,p){var u=q.setFilters[h[2]];if(u)return u(g,n,h,p)}}},x=q.match.POS;for(var G in q.match){q.match[G]=new RegExp(q.match[G].source+/(?![^\[]*\])(?![^\(]*\))/.source); +q.leftMatch[G]=new RegExp(/(^(?:.|\r|\n)*?)/.source+q.match[G].source.replace(/\\(\d+)/g,function(g,h){return"\\"+(h-0+1)}))}var M=function(g,h){g=Array.prototype.slice.call(g,0);if(h){h.push.apply(h,g);return h}return g};try{Array.prototype.slice.call(w.documentElement.childNodes,0)}catch(V){M=function(g,h){h=h||[];if(i.call(g)==="[object Array]")Array.prototype.push.apply(h,g);else if(typeof g.length==="number")for(var n=0,p=g.length;n";var n=w.documentElement;n.insertBefore(g,n.firstChild);if(w.getElementById(h)){q.find.ID=function(p,u,t){if(typeof u.getElementById!=="undefined"&&!t)return(u= +u.getElementById(p[1]))?u.id===p[1]||typeof u.getAttributeNode!=="undefined"&&u.getAttributeNode("id").nodeValue===p[1]?[u]:y:[]};q.filter.ID=function(p,u){var t=typeof p.getAttributeNode!=="undefined"&&p.getAttributeNode("id");return p.nodeType===1&&t&&t.nodeValue===u}}n.removeChild(g);n=g=null})();(function(){var g=w.createElement("div");g.appendChild(w.createComment(""));if(g.getElementsByTagName("*").length>0)q.find.TAG=function(h,n){n=n.getElementsByTagName(h[1]);if(h[1]==="*"){h=[];for(var p= +0;n[p];p++)n[p].nodeType===1&&h.push(n[p]);n=h}return n};g.innerHTML="";if(g.firstChild&&typeof g.firstChild.getAttribute!=="undefined"&&g.firstChild.getAttribute("href")!=="#")q.attrHandle.href=function(h){return h.getAttribute("href",2)};g=null})();w.querySelectorAll&&function(){var g=m,h=w.createElement("div");h.innerHTML="

";if(!(h.querySelectorAll&&h.querySelectorAll(".TEST").length===0)){m=function(p,u,t,I){u=u||w;if(!I&&u.nodeType===9&&!K(u))try{return M(u.querySelectorAll(p), +t)}catch(F){}return g(p,u,t,I)};for(var n in g)m[n]=g[n];h=null}}();(function(){var g=w.createElement("div");g.innerHTML="
";if(!(!g.getElementsByClassName||g.getElementsByClassName("e").length===0)){g.lastChild.className="e";if(g.getElementsByClassName("e").length!==1){q.order.splice(1,0,"CLASS");q.find.CLASS=function(h,n,p){if(typeof n.getElementsByClassName!=="undefined"&&!p)return n.getElementsByClassName(h[1])};g=null}}})();var ba=w.compareDocumentPosition? +function(g,h){return!!(g.compareDocumentPosition(h)&16)}:function(g,h){return g!==h&&(g.contains?g.contains(h):true)},K=function(g){return(g=(g?g.ownerDocument||g:0).documentElement)?g.nodeName!=="HTML":false},ya=function(g,h){var n=[],p="",u;for(h=h.nodeType?[h]:h;u=q.match.PSEUDO.exec(g);){p+=u[0];g=g.replace(q.match.PSEUDO,"")}g=q.relative[g]?g+"*":g;u=0;for(var t=h.length;u=0===d})};c.fn.extend({find:function(a){for(var b= +this.pushStack("","find",a),d=0,f=0,e=this.length;f0)for(var i=d;i +0},closest:function(a,b){if(c.isArray(a)){var d=[],f=this[0],e,i={},j;if(f&&a.length){e=0;for(var r=a.length;e-1:c(f).is(e)){d.push({selector:j,elem:f});delete i[j]}}f=f.parentNode}}return d}var m=c.expr.match.POS.test(a)?c(a,b||this.context):null;return this.map(function(q,x){for(;x&&x.ownerDocument&&x!==b;){if(m?m.index(x)>-1:c(x).is(a))return x; +x=x.parentNode}return null})},index:function(a){if(!a||typeof a==="string")return c.inArray(this[0],a?c(a):this.parent().children());return c.inArray(a.jquery?a[0]:a,this)},add:function(a,b){a=typeof a==="string"?c(a,b||this.context):c.makeArray(a);b=c.merge(this.get(),a);return this.pushStack(S(a[0])||S(b[0])?b:c.unique(b))},andSelf:function(){return this.add(this.prevObject)}});c.each({parent:function(a){return(a=a.parentNode)&&a.nodeType!==11?a:null},parents:function(a){return c.dir(a,"parentNode")}, +parentsUntil:function(a,b,d){return c.dir(a,"parentNode",d)},next:function(a){return c.nth(a,2,"nextSibling")},prev:function(a){return c.nth(a,2,"previousSibling")},nextAll:function(a){return c.dir(a,"nextSibling")},prevAll:function(a){return c.dir(a,"previousSibling")},nextUntil:function(a,b,d){return c.dir(a,"nextSibling",d)},prevUntil:function(a,b,d){return c.dir(a,"previousSibling",d)},siblings:function(a){return c.sibling(a.parentNode.firstChild,a)},children:function(a){return c.sibling(a.firstChild)}, +contents:function(a){return c.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:c.makeArray(a.childNodes)}},function(a,b){c.fn[a]=function(d,f){var e=c.map(this,b,d);gb.test(a)||(f=d);if(f&&typeof f==="string")e=c.filter(f,e);e=this.length>1?c.unique(e):e;if((this.length>1||ib.test(f))&&hb.test(a))e=e.reverse();return this.pushStack(e,a,pa.call(arguments).join(","))}});c.extend({filter:function(a,b,d){if(d)a=":not("+a+")";return c.find.matches(a,b)},dir:function(a,b,d){var f=[];for(a= +a[b];a&&a.nodeType!==9&&(d===y||a.nodeType!==1||!c(a).is(d));){a.nodeType===1&&f.push(a);a=a[b]}return f},nth:function(a,b,d){b=b||1;for(var f=0;a;a=a[d])if(a.nodeType===1&&++f===b)break;return a},sibling:function(a,b){for(var d=[];a;a=a.nextSibling)a.nodeType===1&&a!==b&&d.push(a);return d}});var Sa=/ jQuery\d+="(?:\d+|null)"/g,sa=/^\s+/,Ta=/(<([\w:]+)[^>]*?)\/>/g,jb=/^(?:area|br|col|embed|hr|img|input|link|meta|param)$/i,Ua=/<([\w:]+)/,kb=/"},ca={option:[1,""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]};ca.optgroup=ca.option;ca.tbody=ca.tfoot=ca.colgroup=ca.caption=ca.thead;ca.th= +ca.td;if(!c.support.htmlSerialize)ca._default=[1,"div
","
"];c.fn.extend({text:function(a){if(c.isFunction(a))return this.each(function(b){var d=c(this);d.text(a.call(this,b,d.text()))});if(typeof a!=="object"&&a!==y)return this.empty().append((this[0]&&this[0].ownerDocument||w).createTextNode(a));return c.text(this)},wrapAll:function(a){if(c.isFunction(a))return this.each(function(d){c(this).wrapAll(a.call(this,d))});if(this[0]){var b=c(a,this[0].ownerDocument).eq(0).clone(true);this[0].parentNode&& +b.insertBefore(this[0]);b.map(function(){for(var d=this;d.firstChild&&d.firstChild.nodeType===1;)d=d.firstChild;return d}).append(this)}return this},wrapInner:function(a){if(c.isFunction(a))return this.each(function(b){c(this).wrapInner(a.call(this,b))});return this.each(function(){var b=c(this),d=b.contents();d.length?d.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){c(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){c.nodeName(this,"body")||c(this).replaceWith(this.childNodes)}).end()}, +append:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this)});else if(arguments.length){var a=c(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&& +this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this.nextSibling)});else if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,c(arguments[0]).toArray());return a}},remove:function(a,b){for(var d=0,f;(f=this[d])!=null;d++)if(!a||c.filter(a,[f]).length){if(!b&&f.nodeType===1){c.cleanData(f.getElementsByTagName("*"));c.cleanData([f])}f.parentNode&&f.parentNode.removeChild(f)}return this},empty:function(){for(var a=0,b;(b= +this[a])!=null;a++)for(b.nodeType===1&&c.cleanData(b.getElementsByTagName("*"));b.firstChild;)b.removeChild(b.firstChild);return this},clone:function(a){var b=this.map(function(){if(!c.support.noCloneEvent&&!c.isXMLDoc(this)){var d=this.outerHTML,f=this.ownerDocument;if(!d){d=f.createElement("div");d.appendChild(this.cloneNode(true));d=d.innerHTML}return c.clean([d.replace(Sa,"").replace(/=([^="'>\s]+\/)>/g,'="$1">').replace(sa,"")],f)[0]}else return this.cloneNode(true)});if(a===true){Q(this,b); +Q(this.find("*"),b.find("*"))}return b},html:function(a){if(a===y)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(Sa,""):null;else if(typeof a==="string"&&!Ea.test(a)&&(c.support.leadingWhitespace||!sa.test(a))&&!ca[(Ua.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Ta,Va);try{for(var b=0,d=this.length;b0||e.cacheable||this.length>1?m.cloneNode(true):m)}r.length&&c.each(r,B)}return this}});c.fragments={};c.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){c.fn[a]= +function(d){var f=[];d=c(d);var e=this.length===1&&this[0].parentNode;if(e&&e.nodeType===11&&e.childNodes.length===1&&d.length===1){d[b](this[0]);return this}else{e=0;for(var i=d.length;e0?this.clone(true):this).get();c.fn[b].apply(c(d[e]),j);f=f.concat(j)}return this.pushStack(f,a,d.selector)}}});c.extend({clean:function(a,b,d,f){b=b||w;if(typeof b.createElement==="undefined")b=b.ownerDocument||b[0]&&b[0].ownerDocument||w;for(var e=[],i=0,j;(j=a[i])!=null;i++){if(typeof j==="number")j+= +"";if(j){if(typeof j==="string"&&!lb.test(j))j=b.createTextNode(j);else if(typeof j==="string"){j=j.replace(Ta,Va);var r=(Ua.exec(j)||["",""])[1].toLowerCase(),m=ca[r]||ca._default,q=m[0],x=b.createElement("div");for(x.innerHTML=m[1]+j+m[2];q--;)x=x.lastChild;if(!c.support.tbody){q=kb.test(j);r=r==="table"&&!q?x.firstChild&&x.firstChild.childNodes:m[1]===""&&!q?x.childNodes:[];for(m=r.length-1;m>=0;--m)c.nodeName(r[m],"tbody")&&!r[m].childNodes.length&&r[m].parentNode.removeChild(r[m])}!c.support.leadingWhitespace&& +sa.test(j)&&x.insertBefore(b.createTextNode(sa.exec(j)[0]),x.firstChild);j=x.childNodes}if(j.nodeType)e.push(j);else e=c.merge(e,j)}}if(d)for(i=0;e[i];i++)if(f&&c.nodeName(e[i],"script")&&(!e[i].type||e[i].type.toLowerCase()==="text/javascript"))f.push(e[i].parentNode?e[i].parentNode.removeChild(e[i]):e[i]);else{e[i].nodeType===1&&e.splice.apply(e,[i+1,0].concat(c.makeArray(e[i].getElementsByTagName("script"))));d.appendChild(e[i])}return e},cleanData:function(a){for(var b,d,f=c.cache,e=c.event.special, +i=c.support.deleteExpando,j=0,r;(r=a[j])!=null;j++)if(d=r[c.expando]){b=f[d];if(b.events)for(var m in b.events)e[m]?c.event.remove(r,m):La(r,m,b.handle);if(i)delete r[c.expando];else r.removeAttribute&&r.removeAttribute(c.expando);delete f[d]}}});var mb=/z-?index|font-?weight|opacity|zoom|line-?height/i,Wa=/alpha\([^)]*\)/,Xa=/opacity=([^)]*)/,za=/float/i,Aa=/-([a-z])/ig,nb=/([A-Z])/g,ob=/^-?\d+(?:px)?$/i,pb=/^-?\d/,qb={position:"absolute",visibility:"hidden",display:"block"},rb=["Left","Right"], +sb=["Top","Bottom"],tb=w.defaultView&&w.defaultView.getComputedStyle,Ya=c.support.cssFloat?"cssFloat":"styleFloat",Ba=function(a,b){return b.toUpperCase()};c.fn.css=function(a,b){return k(this,a,b,true,function(d,f,e){if(e===y)return c.curCSS(d,f);if(typeof e==="number"&&!mb.test(f))e+="px";c.style(d,f,e)})};c.extend({style:function(a,b,d){if(!a||a.nodeType===3||a.nodeType===8)return y;if((b==="width"||b==="height")&&parseFloat(d)<0)d=y;var f=a.style||a,e=d!==y;if(!c.support.opacity&&b==="opacity"){if(e){f.zoom= +1;b=parseInt(d,10)+""==="NaN"?"":"alpha(opacity="+d*100+")";a=f.filter||c.curCSS(a,"filter")||"";f.filter=Wa.test(a)?a.replace(Wa,b):b}return f.filter&&f.filter.indexOf("opacity=")>=0?parseFloat(Xa.exec(f.filter)[1])/100+"":""}if(za.test(b))b=Ya;b=b.replace(Aa,Ba);if(e)f[b]=d;return f[b]},css:function(a,b,d,f){if(b==="width"||b==="height"){var e,i=b==="width"?rb:sb;d=function(){e=b==="width"?a.offsetWidth:a.offsetHeight;f!=="border"&&c.each(i,function(){f||(e-=parseFloat(c.curCSS(a,"padding"+this, +true))||0);if(f==="margin")e+=parseFloat(c.curCSS(a,"margin"+this,true))||0;else e-=parseFloat(c.curCSS(a,"border"+this+"Width",true))||0})};a.offsetWidth!==0?d():c.swap(a,qb,d);return Math.max(0,Math.round(e))}return c.curCSS(a,b,d)},curCSS:function(a,b,d){var f,e=a.style;if(!c.support.opacity&&b==="opacity"&&a.currentStyle){f=Xa.test(a.currentStyle.filter||"")?parseFloat(RegExp.$1)/100+"":"";return f===""?"1":f}if(za.test(b))b=Ya;if(!d&&e&&e[b])f=e[b];else if(tb){if(za.test(b))b="float";b=b.replace(nb, +"-$1").toLowerCase();e=a.ownerDocument.defaultView;if(!e)return null;if(a=e.getComputedStyle(a,null))f=a.getPropertyValue(b);if(b==="opacity"&&f==="")f="1"}else if(a.currentStyle){d=b.replace(Aa,Ba);f=a.currentStyle[b]||a.currentStyle[d];if(!ob.test(f)&&pb.test(f)){b=e.left;var i=a.runtimeStyle.left;a.runtimeStyle.left=a.currentStyle.left;e.left=d==="fontSize"?"1em":f||0;f=e.pixelLeft+"px";e.left=b;a.runtimeStyle.left=i}}return f},swap:function(a,b,d){var f={};for(var e in b){f[e]=a.style[e];a.style[e]= +b[e]}d.call(a);for(e in b)a.style[e]=f[e]}});if(c.expr&&c.expr.filters){c.expr.filters.hidden=function(a){var b=a.offsetWidth,d=a.offsetHeight,f=a.nodeName.toLowerCase()==="tr";return b===0&&d===0&&!f?true:b>0&&d>0&&!f?false:c.curCSS(a,"display")==="none"};c.expr.filters.visible=function(a){return!c.expr.filters.hidden(a)}}var ub=s(),vb=//gi,wb=/select|textarea/i,xb=/color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week/i,ma=/=\?(&|$)/, +Ca=/\?/,yb=/(\?|&)_=.*?(&|$)/,zb=/^(\w+:)?\/\/([^\/?#]+)/,Ab=/%20/g,Bb=c.fn.load;c.fn.extend({load:function(a,b,d){if(typeof a!=="string")return Bb.call(this,a);else if(!this.length)return this;var f=a.indexOf(" ");if(f>=0){var e=a.slice(f,a.length);a=a.slice(0,f)}f="GET";if(b)if(c.isFunction(b)){d=b;b=null}else if(typeof b==="object"){b=c.param(b,c.ajaxSettings.traditional);f="POST"}var i=this;c.ajax({url:a,type:f,dataType:"html",data:b,complete:function(j,r){if(r==="success"||r==="notmodified")i.html(e? +c("
").append(j.responseText.replace(vb,"")).find(e):j.responseText);d&&i.each(d,[j.responseText,r,j])}});return this},serialize:function(){return c.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?c.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||wb.test(this.nodeName)||xb.test(this.type))}).map(function(a,b){a=c(this).val();return a==null?null:c.isArray(a)?c.map(a,function(d){return{name:b.name, +value:d}}):{name:b.name,value:a}}).get()}});c.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){c.fn[b]=function(d){return this.bind(b,d)}});c.extend({get:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b=null}return c.ajax({type:"GET",url:a,data:b,success:d,dataType:f})},getScript:function(a,b){return c.get(a,null,b,"script")},getJSON:function(a,b,d){return c.get(a,b,d,"json")},post:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b={}}return c.ajax({type:"POST", +url:a,data:b,success:d,dataType:f})},ajaxSetup:function(a){c.extend(c.ajaxSettings,a)},ajaxSettings:{url:location.href,global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:J.XMLHttpRequest&&(J.location.protocol!=="file:"||!J.ActiveXObject)?function(){return new J.XMLHttpRequest}:function(){try{return new J.ActiveXObject("Microsoft.XMLHTTP")}catch(a){}},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript", +json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},etag:{},ajax:function(a){function b(){e.success&&e.success.call(m,r,j,K);e.global&&f("ajaxSuccess",[K,e])}function d(){e.complete&&e.complete.call(m,K,j);e.global&&f("ajaxComplete",[K,e]);e.global&&!--c.active&&c.event.trigger("ajaxStop")}function f(u,t){(e.context?c(e.context):c.event).trigger(u,t)}var e=c.extend(true,{},c.ajaxSettings,a),i,j,r,m=a&&a.context||e,q=e.type.toUpperCase();if(e.data&&e.processData&& +typeof e.data!=="string")e.data=c.param(e.data,e.traditional);if(e.dataType==="jsonp"){if(q==="GET")ma.test(e.url)||(e.url+=(Ca.test(e.url)?"&":"?")+(e.jsonp||"callback")+"=?");else if(!e.data||!ma.test(e.data))e.data=(e.data?e.data+"&":"")+(e.jsonp||"callback")+"=?";e.dataType="json"}if(e.dataType==="json"&&(e.data&&ma.test(e.data)||ma.test(e.url))){i=e.jsonpCallback||"jsonp"+ub++;if(e.data)e.data=(e.data+"").replace(ma,"="+i+"$1");e.url=e.url.replace(ma,"="+i+"$1");e.dataType="script";J[i]=J[i]|| +function(u){r=u;b();d();J[i]=y;try{delete J[i]}catch(t){}M&&M.removeChild(V)}}if(e.dataType==="script"&&e.cache===null)e.cache=false;if(e.cache===false&&q==="GET"){var x=s(),G=e.url.replace(yb,"$1_="+x+"$2");e.url=G+(G===e.url?(Ca.test(e.url)?"&":"?")+"_="+x:"")}if(e.data&&q==="GET")e.url+=(Ca.test(e.url)?"&":"?")+e.data;e.global&&!c.active++&&c.event.trigger("ajaxStart");x=(x=zb.exec(e.url))&&(x[1]&&x[1]!==location.protocol||x[2]!==location.host);if(e.dataType==="script"&&q==="GET"&&x){var M=w.getElementsByTagName("head")[0]|| +w.documentElement,V=w.createElement("script");V.src=e.url;if(e.scriptCharset)V.charset=e.scriptCharset;if(!i){var T=false;V.onload=V.onreadystatechange=function(){if(!T&&(!this.readyState||this.readyState==="loaded"||this.readyState==="complete")){T=true;b();d();V.onload=V.onreadystatechange=null;M&&V.parentNode&&M.removeChild(V)}}}M.insertBefore(V,M.firstChild);return y}var ba=false,K=e.xhr();if(K){e.username?K.open(q,e.url,e.async,e.username,e.password):K.open(q,e.url,e.async);try{if(e.data||a&& +a.contentType)K.setRequestHeader("Content-Type",e.contentType);if(e.ifModified){c.lastModified[e.url]&&K.setRequestHeader("If-Modified-Since",c.lastModified[e.url]);c.etag[e.url]&&K.setRequestHeader("If-None-Match",c.etag[e.url])}x||K.setRequestHeader("X-Requested-With","XMLHttpRequest");K.setRequestHeader("Accept",e.dataType&&e.accepts[e.dataType]?e.accepts[e.dataType]+", */*":e.accepts._default)}catch(ya){}if(e.beforeSend&&e.beforeSend.call(m,K,e)===false){e.global&&!--c.active&&c.event.trigger("ajaxStop"); +K.abort();return false}e.global&&f("ajaxSend",[K,e]);var g=K.onreadystatechange=function(u){if(!K||K.readyState===0||u==="abort"){ba||d();ba=true;if(K)K.onreadystatechange=c.noop}else if(!ba&&K&&(K.readyState===4||u==="timeout")){ba=true;K.onreadystatechange=c.noop;j=u==="timeout"?"timeout":!c.httpSuccess(K)?"error":e.ifModified&&c.httpNotModified(K,e.url)?"notmodified":"success";var t;if(j==="success")try{r=c.httpData(K,e.dataType,e)}catch(I){j="parsererror";t=I}if(j==="success"||j==="notmodified")i|| +b();else c.handleError(e,K,j,t);d();u==="timeout"&&K.abort();if(e.async)K=null}};try{var h=K.abort;K.abort=function(){K&&h.call(K);g("abort")}}catch(n){}e.async&&e.timeout>0&&setTimeout(function(){K&&!ba&&g("timeout")},e.timeout);try{K.send(q==="POST"||q==="PUT"||q==="DELETE"?e.data:null)}catch(p){c.handleError(e,K,null,p);d()}e.async||g();return K}},handleError:function(a,b,d,f){if(a.error)a.error.call(a.context||a,b,d,f);if(a.global)(a.context?c(a.context):c.event).trigger("ajaxError",[b,a,f])}, +active:0,httpSuccess:function(a){try{return!a.status&&location.protocol==="file:"||a.status>=200&&a.status<300||a.status===304||a.status===1223||a.status===0}catch(b){}return false},httpNotModified:function(a,b){var d=a.getResponseHeader("Last-Modified"),f=a.getResponseHeader("Etag");if(d)c.lastModified[b]=d;if(f)c.etag[b]=f;return a.status===304||a.status===0},httpData:function(a,b,d){var f=a.getResponseHeader("content-type")||"",e=b==="xml"||!b&&f.indexOf("xml")>=0;a=e?a.responseXML:a.responseText; +e&&a.documentElement.nodeName==="parsererror"&&c.error("parsererror");if(d&&d.dataFilter)a=d.dataFilter(a,b);if(typeof a==="string")if(b==="json"||!b&&f.indexOf("json")>=0)a=c.parseJSON(a);else if(b==="script"||!b&&f.indexOf("javascript")>=0)c.globalEval(a);return a},param:function(a,b){function d(j,r){if(c.isArray(r))c.each(r,function(m,q){b||/\[\]$/.test(j)?f(j,q):d(j+"["+(typeof q==="object"||c.isArray(q)?m:"")+"]",q)});else!b&&r!=null&&typeof r==="object"?c.each(r,function(m,q){d(j+"["+m+"]", +q)}):f(j,r)}function f(j,r){r=c.isFunction(r)?r():r;e[e.length]=encodeURIComponent(j)+"="+encodeURIComponent(r)}var e=[];if(b===y)b=c.ajaxSettings.traditional;if(c.isArray(a)||a.jquery)c.each(a,function(){f(this.name,this.value)});else for(var i in a)d(i,a[i]);return e.join("&").replace(Ab,"+")}});var Da={},Cb=/toggle|show|hide/,Db=/^([+-]=)?([\d+-.]+)(.*)$/,ta,Ga=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"], +["opacity"]];c.fn.extend({show:function(a,b){if(a||a===0)return this.animate(N("show",3),a,b);else{a=0;for(b=this.length;a").appendTo("body");f=e.css("display");if(f==="none")f="block";e.remove();Da[d]=f}c.data(this[a],"olddisplay",f)}}a=0;for(b=this.length;a=0;f--)if(d[f].elem===this){b&&d[f](true);d.splice(f,1)}});b||this.dequeue();return this}});c.each({slideDown:N("show",1),slideUp:N("hide",1),slideToggle:N("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(a,b){c.fn[a]=function(d,f){return this.animate(b,d,f)}});c.extend({speed:function(a, +b,d){var f=a&&typeof a==="object"?a:{complete:d||!d&&b||c.isFunction(a)&&a,duration:a,easing:d&&b||b&&!c.isFunction(b)&&b};f.duration=c.fx.off?0:typeof f.duration==="number"?f.duration:c.fx.speeds[f.duration]||c.fx.speeds._default;f.old=f.complete;f.complete=function(){f.queue!==false&&c(this).dequeue();c.isFunction(f.old)&&f.old.call(this)};return f},easing:{linear:function(a,b,d,f){return d+f*a},swing:function(a,b,d,f){return(-Math.cos(a*Math.PI)/2+0.5)*f+d}},timers:[],fx:function(a,b,d){this.options= +b;this.elem=a;this.prop=d;if(!b.orig)b.orig={}}});c.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this);(c.fx.step[this.prop]||c.fx.step._default)(this);if((this.prop==="height"||this.prop==="width")&&this.elem.style)this.elem.style.display="block"},cur:function(a){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];return(a=parseFloat(c.css(this.elem,this.prop,a)))&&a>-10000?a:parseFloat(c.curCSS(this.elem, +this.prop))||0},custom:function(a,b,d){function f(i){return e.step(i)}this.startTime=s();this.start=a;this.end=b;this.unit=d||this.unit||"px";this.now=this.start;this.pos=this.state=0;var e=this;f.elem=this.elem;if(f()&&c.timers.push(f)&&!ta)ta=setInterval(c.fx.tick,13)},show:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.show=true;this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur());c(this.elem).show()},hide:function(){this.options.orig[this.prop]= +c.style(this.elem,this.prop);this.options.hide=true;this.custom(this.cur(),0)},step:function(a){var b=s(),d=true;if(a||b>=this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;for(var f in this.options.curAnim)if(this.options.curAnim[f]!==true)d=false;if(d){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;a=c.data(this.elem,"olddisplay");this.elem.style.display=a?a:this.options.display;if(c.css(this.elem, +"display")==="none")this.elem.style.display="block"}this.options.hide&&c(this.elem).hide();if(this.options.hide||this.options.show)for(var e in this.options.curAnim)c.style(this.elem,e,this.options.orig[e]);this.options.complete.call(this.elem)}return false}else{e=b-this.startTime;this.state=e/this.options.duration;a=this.options.easing||(c.easing.swing?"swing":"linear");this.pos=c.easing[this.options.specialEasing&&this.options.specialEasing[this.prop]||a](this.state,e,0,1,this.options.duration); +this.now=this.start+(this.end-this.start)*this.pos;this.update()}return true}};c.extend(c.fx,{tick:function(){for(var a=c.timers,b=0;b +
+
+
Back
+
+
+ +