Permalink
Browse files

initial import

  • Loading branch information...
0 parents commit a67ae9fac65fb3503cf350117556277383360ea2 @drnic drnic committed Apr 26, 2008
4 History.txt
@@ -0,0 +1,4 @@
+== 0.0.1 2008-04-26
+
+* 1 major enhancement:
+ * Initial release
20 License.txt
@@ -0,0 +1,20 @@
+Copyright (c) 2008 Dr Nic Williams
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
13 README.txt
@@ -0,0 +1,13 @@
+Github Badges
+
+Description:
+ This JavaScript project ...
+
+Example:
+ ...
+
+More information:
+ http://NOTE-ENTER-URL.com
+
+Author:
+ Dr Nic Williams, drnicwilliams@gmail.com
121 Rakefile
@@ -0,0 +1,121 @@
+require 'rubygems'
+begin
+ require 'rake'
+rescue LoadError
+ puts 'This script should only be accessed via the "rake" command.'
+ puts 'Installation: gem install rake -y'
+ exit
+end
+require 'rake'
+require 'rake/clean'
+require 'rake/packagetask'
+
+$:.unshift File.dirname(__FILE__) + "/lib"
+
+APP_VERSION = '0.0.1'
+APP_NAME = 'github_badges'
+RUBYFORGE_PROJECT = APP_NAME
+APP_FILE_NAME= "#{APP_NAME}.js"
+
+APP_ROOT = File.expand_path(File.dirname(__FILE__))
+APP_SRC_DIR = File.join(APP_ROOT, 'src')
+APP_DIST_DIR = File.join(APP_ROOT, 'dist')
+APP_PKG_DIR = File.join(APP_ROOT, 'pkg')
+
+
+unless ENV['rakefile_just_config']
+
+task :default => [:dist, :package, :clean_package_source]
+
+desc "Builds the distribution"
+task :dist do
+ $:.unshift File.join(APP_ROOT, 'lib')
+ require 'protodoc'
+ require 'fileutils'
+ FileUtils.mkdir_p APP_DIST_DIR
+
+ Dir.chdir(APP_SRC_DIR) do
+ File.open(File.join(APP_DIST_DIR, APP_FILE_NAME), 'w+') do |dist|
+ dist << Protodoc::Preprocessor.new(APP_FILE_NAME)
+ end
+ end
+ Dir.chdir(APP_DIST_DIR) do
+ FileUtils.copy_file APP_FILE_NAME, "#{APP_NAME}-#{APP_VERSION}.js"
+ end
+ if File.directory?("website")
+ FileUtils.mkdir_p "website/dist"
+ FileUtils.copy_file "dist/#{APP_FILE_NAME}", "website/dist/#{APP_FILE_NAME}"
+ FileUtils.copy_file "dist/#{APP_FILE_NAME}", "website/dist/#{APP_NAME}-#{APP_VERSION}.js"
+ end
+end
+
+Rake::PackageTask.new(APP_NAME, APP_VERSION) do |package|
+ package.need_tar_gz = true
+ package.package_dir = APP_PKG_DIR
+ package.package_files.include(
+ '[A-Z]*',
+ 'config/*.sample',
+ "dist/#{APP_FILE_NAME}",
+ 'lib/**',
+ 'src/**',
+ 'script/**',
+ 'tasks/**',
+ 'test/**',
+ 'website/**'
+ )
+end
+
+desc "Builds the distribution, runs the JavaScript unit + functional tests and collects their results."
+task :test => [:dist, :test_units, :test_functionals]
+
+require 'jstest'
+desc "Runs all the JavaScript unit tests and collects the results"
+JavaScriptTestTask.new(:test_units, 4711) do |t|
+ testcases = ENV['TESTCASES']
+ tests_to_run = ENV['TESTS'] && ENV['TESTS'].split(',')
+ browsers_to_test = ENV['BROWSERS'] && ENV['BROWSERS'].split(',')
+
+ t.mount("/dist")
+ t.mount("/src")
+ t.mount("/test")
+
+ Dir["test/unit/*_test.html"].sort.each do |test_file|
+ tests = testcases ? { :url => "/#{test_file}", :testcases => testcases } : "/#{test_file}"
+ test_filename = test_file[/.*\/(.+?)\.html/, 1]
+ t.run(tests) unless tests_to_run && !tests_to_run.include?(test_filename)
+ end
+
+ %w( safari firefox ie konqueror opera ).each do |browser|
+ t.browser(browser.to_sym) unless browsers_to_test && !browsers_to_test.include?(browser)
+ end
+end
+
+desc "Runs all the JavaScript functional tests and collects the results"
+JavaScriptTestTask.new(:test_functionals, 4712) do |t|
+ testcases = ENV['TESTCASES']
+ tests_to_run = ENV['TESTS'] && ENV['TESTS'].split(',')
+ browsers_to_test = ENV['BROWSERS'] && ENV['BROWSERS'].split(',')
+
+ t.mount("/dist")
+ t.mount("/src")
+ t.mount("/test")
+
+ Dir["test/functional/*_test.html"].sort.each do |test_file|
+ tests = testcases ? { :url => "/#{test_file}", :testcases => testcases } : "/#{test_file}"
+ test_filename = test_file[/.*\/(.+?)\.html/, 1]
+ t.run(tests) unless tests_to_run && !tests_to_run.include?(test_filename)
+ end
+
+ %w( safari firefox ie konqueror opera ).each do |browser|
+ t.browser(browser.to_sym) unless browsers_to_test && !browsers_to_test.include?(browser)
+ end
+end
+
+
+task :clean_package_source do
+ rm_rf File.join(APP_PKG_DIR, "#{APP_NAME}-#{APP_VERSION}")
+end
+
+Dir['tasks/**/*.rake'].each { |rake| load rake }
+
+end
15 config/javascript_test_autotest.yml
@@ -0,0 +1,15 @@
+# This file has been copied here by the javascript_test_autotest plugin.
+# Comment/uncomment the browsers you wish to autotest with.
+# Same schema as per selenium-on-rails plugin, which is nice.
+browsers:
+ # Windows
+ # firefox: 'c:\Program Files\Mozilla Firefox\firefox.exe'
+ # ie: 'c:\Program Files\Internet Explorer\iexplore.exe'
+
+ # Mac OS X
+ # firefox: '/Applications/Firefox.app/Contents/MacOS/firefox-bin'
+ safari: '/Applications/Safari.app/Contents/MacOS/Safari'
+
+ # Unix
+ # ?
+ # ?
382 lib/jstest.rb
@@ -0,0 +1,382 @@
+require 'rake/tasklib'
+require 'thread'
+require 'webrick'
+require 'fileutils'
+include FileUtils
+
+class Browser
+ def supported?; true; end
+ def setup ; end
+ def open(url) ; end
+ def teardown ; end
+
+ def host
+ require 'rbconfig'
+ Config::CONFIG['host']
+ end
+
+ def macos?
+ host.include?('darwin')
+ end
+
+ def windows?
+ host.include?('mswin')
+ end
+
+ def linux?
+ host.include?('linux')
+ end
+
+ def applescript(script)
+ raise "Can't run AppleScript on #{host}" unless macos?
+ system "osascript -e '#{script}' 2>&1 >/dev/null"
+ end
+end
+
+class FirefoxBrowser < Browser
+ def initialize(path=File.join(ENV['ProgramFiles'] || 'c:\Program Files', '\Mozilla Firefox\firefox.exe'))
+ @path = path
+ end
+
+ def visit(url)
+ system("open -a Firefox '#{url}'") if macos?
+ system("#{@path} #{url}") if windows?
+ system("firefox #{url}") if linux?
+ end
+
+ def to_s
+ "Firefox"
+ end
+end
+
+class SafariBrowser < Browser
+ def supported?
+ macos?
+ end
+
+ def setup
+ applescript('tell application "Safari" to make new document')
+ end
+
+ def visit(url)
+ applescript('tell application "Safari" to set URL of front document to "' + url + '"')
+ end
+
+ def teardown
+ #applescript('tell application "Safari" to close front document')
+ end
+
+ def to_s
+ "Safari"
+ end
+end
+
+class IEBrowser < Browser
+ def setup
+ require 'win32ole' if windows?
+ end
+
+ def supported?
+ windows?
+ end
+
+ def visit(url)
+ if windows?
+ ie = WIN32OLE.new('InternetExplorer.Application')
+ ie.visible = true
+ ie.Navigate(url)
+ while ie.ReadyState != 4 do
+ sleep(1)
+ end
+ end
+ end
+
+ def to_s
+ "Internet Explorer"
+ end
+end
+
+class KonquerorBrowser < Browser
+ @@configDir = File.join((ENV['HOME'] || ''), '.kde', 'share', 'config')
+ @@globalConfig = File.join(@@configDir, 'kdeglobals')
+ @@konquerorConfig = File.join(@@configDir, 'konquerorrc')
+
+ def supported?
+ linux?
+ end
+
+ # Forces KDE's default browser to be Konqueror during the tests, and forces
+ # Konqueror to open external URL requests in new tabs instead of a new
+ # window.
+ def setup
+ cd @@configDir, :verbose => false do
+ copy @@globalConfig, "#{@@globalConfig}.bak", :preserve => true, :verbose => false
+ copy @@konquerorConfig, "#{@@konquerorConfig}.bak", :preserve => true, :verbose => false
+ # Too lazy to write it in Ruby... Is sed dependency so bad?
+ system "sed -ri /^BrowserApplication=/d '#{@@globalConfig}'"
+ system "sed -ri /^KonquerorTabforExternalURL=/s:false:true: '#{@@konquerorConfig}'"
+ end
+ end
+
+ def teardown
+ cd @@configDir, :verbose => false do
+ copy "#{@@globalConfig}.bak", @@globalConfig, :preserve => true, :verbose => false
+ copy "#{@@konquerorConfig}.bak", @@konquerorConfig, :preserve => true, :verbose => false
+ end
+ end
+
+ def visit(url)
+ system("kfmclient openURL #{url}")
+ end
+
+ def to_s
+ "Konqueror"
+ end
+end
+
+class OperaBrowser < Browser
+ def initialize(path='c:\Program Files\Opera\Opera.exe')
+ @path = path
+ end
+
+ def setup
+ if windows?
+ puts %{
+ MAJOR ANNOYANCE on Windows.
+ You have to shut down Opera manually after each test
+ for the script to proceed.
+ Any suggestions on fixing this is GREATLY appreciated!
+ Thank you for your understanding.
+ }
+ end
+ end
+
+ def visit(url)
+ applescript('tell application "Opera" to GetURL "' + url + '"') if macos?
+ system("#{@path} #{url}") if windows?
+ system("opera #{url}") if linux?
+ end
+
+ def to_s
+ "Opera"
+ end
+end
+
+# shut up, webrick :-)
+class ::WEBrick::HTTPServer
+ def access_log(config, req, res)
+ # nop
+ end
+end
+
+class ::WEBrick::BasicLog
+ def log(level, data)
+ # nop
+ end
+end
+
+class WEBrick::HTTPResponse
+ alias send send_response
+ def send_response(socket)
+ send(socket) unless fail_silently?
+ end
+
+ def fail_silently?
+ @fail_silently
+ end
+
+ def fail_silently
+ @fail_silently = true
+ end
+end
+
+class WEBrick::HTTPRequest
+ def to_json
+ headers = []
+ each { |k, v| headers.push "#{k.inspect}: #{v.inspect}" }
+ headers = "{" << headers.join(', ') << "}"
+ %({ "headers": #{headers}, "body": #{body.inspect}, "method": #{request_method.inspect} })
+ end
+end
+
+class WEBrick::HTTPServlet::AbstractServlet
+ def prevent_caching(res)
+ res['ETag'] = nil
+ res['Last-Modified'] = Time.now + 100**4
+ res['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0'
+ res['Pragma'] = 'no-cache'
+ res['Expires'] = Time.now - 100**4
+ end
+end
+
+class BasicServlet < WEBrick::HTTPServlet::AbstractServlet
+ def do_GET(req, res)
+ prevent_caching(res)
+ res['Content-Type'] = "text/plain"
+
+ req.query.each do |k, v|
+ res[k] = v unless k == 'responseBody'
+ end
+ res.body = req.query["responseBody"]
+
+ raise WEBrick::HTTPStatus::OK
+ end
+
+ def do_POST(req, res)
+ do_GET(req, res)
+ end
+end
+
+class SlowServlet < BasicServlet
+ def do_GET(req, res)
+ sleep(2)
+ super
+ end
+end
+
+class DownServlet < BasicServlet
+ def do_GET(req, res)
+ res.fail_silently
+ end
+end
+
+class InspectionServlet < BasicServlet
+ def do_GET(req, res)
+ prevent_caching(res)
+ res['Content-Type'] = "application/json"
+ res.body = req.to_json
+ raise WEBrick::HTTPStatus::OK
+ end
+end
+
+class NonCachingFileHandler < WEBrick::HTTPServlet::FileHandler
+ def do_GET(req, res)
+ super
+ set_default_content_type(res, req.path)
+ prevent_caching(res)
+ end
+
+ def set_default_content_type(res, path)
+ res['Content-Type'] = case path
+ when /\.js$/ then 'text/javascript'
+ when /\.html$/ then 'text/html'
+ when /\.css$/ then 'text/css'
+ else 'text/plain'
+ end
+ end
+end
+
+class JavaScriptTestTask < ::Rake::TaskLib
+
+ def initialize(name=:test, port=4711)
+ @name = name
+ @tests = []
+ @browsers = []
+ @port = port
+ @queue = Queue.new
+
+ @server = WEBrick::HTTPServer.new(:Port => @port) # TODO: make port configurable
+ @server.mount_proc("/results") do |req, res|
+ @queue.push({
+ :tests => req.query['tests'].to_i,
+ :assertions => req.query['assertions'].to_i,
+ :failures => req.query['failures'].to_i,
+ :errors => req.query['errors'].to_i
+ })
+ res.body = "OK"
+ end
+ @server.mount("/response", BasicServlet)
+ @server.mount("/slow", SlowServlet)
+ @server.mount("/down", DownServlet)
+ @server.mount("/inspect", InspectionServlet)
+ yield self if block_given?
+ define
+ end
+
+ def define
+ task @name do
+ trap("INT") { @server.shutdown }
+ t = Thread.new { @server.start }
+
+ # run all combinations of browsers and tests
+ @browsers.each do |browser|
+ if browser.supported?
+ t0 = Time.now
+ results = {:tests => 0, :assertions => 0, :failures => 0, :errors => 0}
+ errors = []
+ failures = []
+ browser.setup
+ puts "\nStarted tests in #{browser}"
+ @tests.each do |test|
+ params = "resultsURL=http://localhost:#{@port}/results&t=" + ("%.6f" % Time.now.to_f)
+ if test.is_a?(Hash)
+ params << "&tests=#{test[:testcases]}" if test[:testcases]
+ test = test[:url]
+ end
+ browser.visit("http://localhost:#{@port}#{test}?#{params}")
+
+ result = @queue.pop
+ result.each { |k, v| results[k] += v }
+ value = "."
+
+ if result[:failures] > 0
+ value = "F"
+ failures.push(test)
+ end
+
+ if result[:errors] > 0
+ value = "E"
+ errors.push(test)
+ end
+
+ print value
+ end
+
+ puts "\nFinished in #{(Time.now - t0).round.to_s} seconds."
+ puts " Failures: #{failures.join(', ')}" unless failures.empty?
+ puts " Errors: #{errors.join(', ')}" unless errors.empty?
+ puts "#{results[:tests]} tests, #{results[:assertions]} assertions, #{results[:failures]} failures, #{results[:errors]} errors"
+ browser.teardown
+ else
+ puts "\nSkipping #{browser}, not supported on this OS"
+ end
+ end
+
+ @server.shutdown
+ t.join
+ end
+ end
+
+ def mount(path, dir=nil)
+ dir = Dir.pwd + path unless dir
+
+ # don't cache anything in our tests
+ @server.mount(path, NonCachingFileHandler, dir)
+ end
+
+ # test should be specified as a url or as a hash of the form
+ # {:url => "url", :testcases => "testFoo,testBar"}
+ def run(test)
+ @tests<<test
+ end
+
+ def browser(browser)
+ browser =
+ case(browser)
+ when :firefox
+ FirefoxBrowser.new
+ when :safari
+ SafariBrowser.new
+ when :ie
+ IEBrowser.new
+ when :konqueror
+ KonquerorBrowser.new
+ when :opera
+ OperaBrowser.new
+ else
+ browser
+ end
+
+ @browsers<<browser
+ end
+end
36 lib/protodoc.rb
@@ -0,0 +1,36 @@
+require 'erb'
+
+class String
+ def lines
+ split $/
+ end
+
+ def strip_whitespace_at_line_ends
+ lines.map {|line| line.gsub(/\s+$/, '')} * $/
+ end
+end
+
+module Protodoc
+ module Environment
+ def include(*filenames)
+ filenames.map {|filename| Preprocessor.new(filename).to_s}.join("\n")
+ end
+ end
+
+ class Preprocessor
+ include Environment
+
+ def initialize(filename)
+ @filename = File.expand_path(filename)
+ @template = ERB.new(IO.read(@filename), nil, '%')
+ end
+
+ def to_s
+ @template.result(binding).strip_whitespace_at_line_ends
+ end
+ end
+end
+
+if __FILE__ == $0
+ print Protodoc::Preprocessor.new(ARGV.first)
+end
14 script/destroy
@@ -0,0 +1,14 @@
+#!/usr/bin/env ruby
+APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
+
+begin
+ require 'rubigen'
+rescue LoadError
+ require 'rubygems'
+ require 'rubigen'
+end
+require 'rubigen/scripts/destroy'
+
+ARGV.shift if ['--help', '-h'].include?(ARGV[0])
+RubiGen::Base.use_component_sources! [:javascript, :javascript_test, :newjs, :newjs_theme]
+RubiGen::Scripts::Destroy.new.run(ARGV)
14 script/generate
@@ -0,0 +1,14 @@
+#!/usr/bin/env ruby
+APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
+
+begin
+ require 'rubigen'
+rescue LoadError
+ require 'rubygems'
+ require 'rubigen'
+end
+require 'rubigen/scripts/generate'
+
+ARGV.shift if ['--help', '-h'].include?(ARGV[0])
+RubiGen::Base.use_component_sources! [:javascript, :javascript_test, :newjs, :newjs_theme]
+RubiGen::Scripts::Generate.new.run(ARGV)
1 script/js_autotest
@@ -0,0 +1 @@
+script/rstakeout "rake test:recent:javascript" test/unit/*_test.html src/*.js
98 script/rstakeout
@@ -0,0 +1,98 @@
+#!/usr/local/bin/ruby -w
+
+
+config = File.dirname(__FILE__) + "/../config/javascript_test_autotest.yml"
+unless File.exists?(config)
+ puts <<-EOS
+Edit config/javascript_test_autotest.yml for the browser(s) to use for autotesting.
+See config/javascript_test_autotest.yml.sample for examples.
+EOS
+ exit
+end
+
+##
+# Originally by Mike Clark.
+#
+# From http://www.pragmaticautomation.com/cgi-bin/pragauto.cgi/Monitor/StakingOutFileChanges.rdoc
+#
+# Runs a user-defined command when files are modified.
+#
+# Like autotest, but more customizable. This is useful when you want to do
+# something other than run tests. For example, generate a PDF book, run
+# a single test, or run a legacy Test::Unit suite in an app that also
+# has an rSpec suite.
+#
+# Can use Ruby's Dir[] to get file glob. Quote your args to take advantage of this.
+#
+# rstakeout 'rake test:recent' **/*.rb
+# => Only watches Ruby files one directory down (no quotes)
+#
+# rstakeout 'rake test:recent' '**/*.rb'
+# => Watches all Ruby files in all directories and subdirectories
+#
+# Modified (with permission) by Geoffrey Grosenbach to call growlnotify for
+# rspec and Test::Unit output.
+#
+# See the PeepCode screencast on rSpec or other blog articles for instructions on
+# setting up growlnotify.
+
+def growl(title, msg, img, pri=0, sticky="")
+ system "growlnotify -n autotest --image ~/.autotest_images/#{img} -p #{pri} -m #{msg.inspect} #{title} #{sticky}"
+end
+
+def self.growl_fail(output)
+ growl "FAIL", "#{output}", "fail.png", 2
+end
+
+def self.growl_pass(output)
+ growl "Pass", "#{output}", "pass.png"
+end
+
+command = ARGV.shift
+files = {}
+
+ARGV.each do |arg|
+ Dir[arg].each { |file|
+ files[file] = File.mtime(file)
+ }
+end
+
+puts "Watching #{files.keys.join(', ')}\n\nFiles: #{files.keys.length}"
+
+trap('INT') do
+ puts "\nQuitting..."
+ exit
+end
+
+
+loop do
+
+ sleep 1
+
+ changed_file, last_changed = files.find { |file, last_changed|
+ File.mtime(file) > last_changed
+ }
+
+ if changed_file
+ files[changed_file] = File.mtime(changed_file)
+ puts "=> #{changed_file} changed, running #{command}"
+ results = `#{command}`
+ puts results
+
+ if results.include? 'tests'
+ output = results.slice(/(\d+)\s+tests?,\s*(\d+)\s+assertions?,\s*(\d+)\s+failures?(,\s*(\d+)\s+errors)?/)
+ if output
+ $~[3].to_i + $~[5].to_i > 0 ? growl_fail(output) : growl_pass(output)
+ end
+ else
+ output = results.slice(/(\d+)\s+examples?,\s*(\d+)\s+failures?(,\s*(\d+)\s+not implemented)?/)
+ if output
+ $~[2].to_i > 0 ? growl_fail(output) : growl_pass(output)
+ end
+ end
+ # TODO Generic growl notification for other actions
+
+ puts "=> done"
+ end
+
+end
8 src/HEADER
@@ -0,0 +1,8 @@
+/* Github Badges, version <%= APP_VERSION %>
+ * (c) 2008 Dr Nic Williams
+ *
+ * Github Badges is freely distributable under
+ * the terms of an MIT-style license.
+ * For details, see the web site: http://NOTE-ENTER-URL.com
+ *
+ *--------------------------------------------------------------------------*/
28 src/ext/jquery.ajax_cache.js
@@ -0,0 +1,28 @@
+/* Copyright (c) 2007 Dr Nic Williams (drnicwilliams@gmail.com || http://drnicwilliams.com)
+ * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
+ *
+ * Version: @VERSION
+ * Requires jQuery 1.2.3+
+ * Docs: http://docs.jquery.com/Plugins/ajax_cache
+ * Dependencies: jCache 1.0.2 - http://plugins.jquery.com/project/jCache
+ *
+ * Description: Provides a $.getCachedJSON method that has the same API as the standard $.getJSON
+ * but the url+args=>json is cached on the page. Subsequent calls to same url+args will return the
+ * original result without an $.ajax call being made.
+ */
+
+(function($) {
+ this.version = '0.1';
+
+ $.getCachedJSON = function(url, args, success) {
+ if (url) {
+ var full_url = url + (args ? "" : "?" + $.param(args));
+ var json = $.jCache.getItem(full_url);
+ }
+ if (!json) $.getJSON(url, args, function(json) {
+ $.jCache.setItem(full_url, json);
+ success(json);
+ });
+ else success(json);
+ };
+})(jQuery);
869 src/ext/jquery.form.js
@@ -0,0 +1,869 @@
+/*
+ * jQuery Form Plugin
+ * version: 2.07 (03/04/2008)
+ * @requires jQuery v1.2.2 or later
+ *
+ * Examples at: http://malsup.com/jquery/form/
+ * Dual licensed under the MIT and GPL licenses:
+ * http://www.opensource.org/licenses/mit-license.php
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Revision: $Id$
+ */
+ (function($) {
+/**
+ * ajaxSubmit() provides a mechanism for submitting an HTML form using AJAX.
+ *
+ * ajaxSubmit accepts a single argument which can be either a success callback function
+ * or an options Object. If a function is provided it will be invoked upon successful
+ * completion of the submit and will be passed the response from the server.
+ * If an options Object is provided, the following attributes are supported:
+ *
+ * target: Identifies the element(s) in the page to be updated with the server response.
+ * This value may be specified as a jQuery selection string, a jQuery object,
+ * or a DOM element.
+ * default value: null
+ *
+ * url: URL to which the form data will be submitted.
+ * default value: value of form's 'action' attribute
+ *
+ * type: The method in which the form data should be submitted, 'GET' or 'POST'.
+ * default value: value of form's 'method' attribute (or 'GET' if none found)
+ *
+ * data: Additional data to add to the request, specified as key/value pairs (see $.ajax).
+ *
+ * beforeSubmit: Callback method to be invoked before the form is submitted.
+ * default value: null
+ *
+ * success: Callback method to be invoked after the form has been successfully submitted
+ * and the response has been returned from the server
+ * default value: null
+ *
+ * dataType: Expected dataType of the response. One of: null, 'xml', 'script', or 'json'
+ * default value: null
+ *
+ * semantic: Boolean flag indicating whether data must be submitted in semantic order (slower).
+ * default value: false
+ *
+ * resetForm: Boolean flag indicating whether the form should be reset if the submit is successful
+ *
+ * clearForm: Boolean flag indicating whether the form should be cleared if the submit is successful
+ *
+ *
+ * The 'beforeSubmit' callback can be provided as a hook for running pre-submit logic or for
+ * validating the form data. If the 'beforeSubmit' callback returns false then the form will
+ * not be submitted. The 'beforeSubmit' callback is invoked with three arguments: the form data
+ * in array format, the jQuery object, and the options object passed into ajaxSubmit.
+ * The form data array takes the following form:
+ *
+ * [ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ]
+ *
+ * If a 'success' callback method is provided it is invoked after the response has been returned
+ * from the server. It is passed the responseText or responseXML value (depending on dataType).
+ * See jQuery.ajax for further details.
+ *
+ *
+ * The dataType option provides a means for specifying how the server response should be handled.
+ * This maps directly to the jQuery.httpData method. The following values are supported:
+ *
+ * 'xml': if dataType == 'xml' the server response is treated as XML and the 'success'
+ * callback method, if specified, will be passed the responseXML value
+ * 'json': if dataType == 'json' the server response will be evaluted and passed to
+ * the 'success' callback, if specified
+ * 'script': if dataType == 'script' the server response is evaluated in the global context
+ *
+ *
+ * Note that it does not make sense to use both the 'target' and 'dataType' options. If both
+ * are provided the target will be ignored.
+ *
+ * The semantic argument can be used to force form serialization in semantic order.
+ * This is normally true anyway, unless the form contains input elements of type='image'.
+ * If your form must be submitted with name/value pairs in semantic order and your form
+ * contains an input of type='image" then pass true for this arg, otherwise pass false
+ * (or nothing) to avoid the overhead for this logic.
+ *
+ *
+ * When used on its own, ajaxSubmit() is typically bound to a form's submit event like this:
+ *
+ * $("#form-id").submit(function() {
+ * $(this).ajaxSubmit(options);
+ * return false; // cancel conventional submit
+ * });
+ *
+ * When using ajaxForm(), however, this is done for you.
+ *
+ * @example
+ * $('#myForm').ajaxSubmit(function(data) {
+ * alert('Form submit succeeded! Server returned: ' + data);
+ * });
+ * @desc Submit form and alert server response
+ *
+ *
+ * @example
+ * var options = {
+ * target: '#myTargetDiv'
+ * };
+ * $('#myForm').ajaxSubmit(options);
+ * @desc Submit form and update page element with server response
+ *
+ *
+ * @example
+ * var options = {
+ * success: function(responseText) {
+ * alert(responseText);
+ * }
+ * };
+ * $('#myForm').ajaxSubmit(options);
+ * @desc Submit form and alert the server response
+ *
+ *
+ * @example
+ * var options = {
+ * beforeSubmit: function(formArray, jqForm) {
+ * if (formArray.length == 0) {
+ * alert('Please enter data.');
+ * return false;
+ * }
+ * }
+ * };
+ * $('#myForm').ajaxSubmit(options);
+ * @desc Pre-submit validation which aborts the submit operation if form data is empty
+ *
+ *
+ * @example
+ * var options = {
+ * url: myJsonUrl.php,
+ * dataType: 'json',
+ * success: function(data) {
+ * // 'data' is an object representing the the evaluated json data
+ * }
+ * };
+ * $('#myForm').ajaxSubmit(options);
+ * @desc json data returned and evaluated
+ *
+ *
+ * @example
+ * var options = {
+ * url: myXmlUrl.php,
+ * dataType: 'xml',
+ * success: function(responseXML) {
+ * // responseXML is XML document object
+ * var data = $('myElement', responseXML).text();
+ * }
+ * };
+ * $('#myForm').ajaxSubmit(options);
+ * @desc XML data returned from server
+ *
+ *
+ * @example
+ * var options = {
+ * resetForm: true
+ * };
+ * $('#myForm').ajaxSubmit(options);
+ * @desc submit form and reset it if successful
+ *
+ * @example
+ * $('#myForm).submit(function() {
+ * $(this).ajaxSubmit();
+ * return false;
+ * });
+ * @desc Bind form's submit event to use ajaxSubmit
+ *
+ *
+ * @name ajaxSubmit
+ * @type jQuery
+ * @param options object literal containing options which control the form submission process
+ * @cat Plugins/Form
+ * @return jQuery
+ */
+$.fn.ajaxSubmit = function(options) {
+ if (typeof options == 'function')
+ options = { success: options };
+
+ options = $.extend({
+ url: this.attr('action') || window.location.toString(),
+ type: this.attr('method') || 'GET'
+ }, options || {});
+
+ // hook for manipulating the form data before it is extracted;
+ // convenient for use with rich editors like tinyMCE or FCKEditor
+ var veto = {};
+ this.trigger('form-pre-serialize', [this, options, veto]);
+ if (veto.veto) return this;
+
+ var a = this.formToArray(options.semantic);
+ if (options.data) {
+ options.extraData = options.data;
+ for (var n in options.data)
+ a.push( { name: n, value: options.data[n] } );
+ }
+
+ // give pre-submit callback an opportunity to abort the submit
+ if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) return this;
+
+ // fire vetoable 'validate' event
+ this.trigger('form-submit-validate', [a, this, options, veto]);
+ if (veto.veto) return this;
+
+ var q = $.param(a);
+
+ if (options.type.toUpperCase() == 'GET') {
+ options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q;
+ options.data = null; // data is null for 'get'
+ }
+ else
+ options.data = q; // data is the query string for 'post'
+
+ var $form = this, callbacks = [];
+ if (options.resetForm) callbacks.push(function() { $form.resetForm(); });
+ if (options.clearForm) callbacks.push(function() { $form.clearForm(); });
+
+ // perform a load on the target only if dataType is not provided
+ if (!options.dataType && options.target) {
+ var oldSuccess = options.success || function(){};
+ callbacks.push(function(data) {
+ $(options.target).html(data).each(oldSuccess, arguments);
+ });
+ }
+ else if (options.success)
+ callbacks.push(options.success);
+
+ options.success = function(data, status) {
+ for (var i=0, max=callbacks.length; i < max; i++)
+ callbacks[i](data, status, $form);
+ };
+
+ // are there files to upload?
+ var files = $('input:file', this).fieldValue();
+ var found = false;
+ for (var j=0; j < files.length; j++)
+ if (files[j])
+ found = true;
+
+ // options.iframe allows user to force iframe mode
+ if (options.iframe || found) {
+ // hack to fix Safari hang (thanks to Tim Molendijk for this)
+ // see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d
+ if ($.browser.safari && options.closeKeepAlive)
+ $.get(options.closeKeepAlive, fileUpload);
+ else
+ fileUpload();
+ }
+ else
+ $.ajax(options);
+
+ // fire 'notify' event
+ this.trigger('form-submit-notify', [this, options]);
+ return this;
+
+
+ // private function for handling file uploads (hat tip to YAHOO!)
+ function fileUpload() {
+ var form = $form[0];
+ var opts = $.extend({}, $.ajaxSettings, options);
+
+ var id = 'jqFormIO' + (new Date().getTime());
+ var $io = $('<iframe id="' + id + '" name="' + id + '" />');
+ var io = $io[0];
+ var op8 = $.browser.opera && window.opera.version() < 9;
+ if ($.browser.msie || op8) io.src = 'javascript:false;document.write("");';
+ $io.css({ position: 'absolute', top: '-1000px', left: '-1000px' });
+
+ var xhr = { // mock object
+ responseText: null,
+ responseXML: null,
+ status: 0,
+ statusText: 'n/a',
+ getAllResponseHeaders: function() {},
+ getResponseHeader: function() {},
+ setRequestHeader: function() {}
+ };
+
+ var g = opts.global;
+ // trigger ajax global events so that activity/block indicators work like normal
+ if (g && ! $.active++) $.event.trigger("ajaxStart");
+ if (g) $.event.trigger("ajaxSend", [xhr, opts]);
+
+ var cbInvoked = 0;
+ var timedOut = 0;
+
+ // take a breath so that pending repaints get some cpu time before the upload starts
+ setTimeout(function() {
+ // make sure form attrs are set
+ var t = $form.attr('target'), a = $form.attr('action');
+ $form.attr({
+ target: id,
+ encoding: 'multipart/form-data',
+ enctype: 'multipart/form-data',
+ method: 'POST',
+ action: opts.url
+ });
+
+ // support timout
+ if (opts.timeout)
+ setTimeout(function() { timedOut = true; cb(); }, opts.timeout);
+
+ // add "extra" data to form if provided in options
+ var extraInputs = [];
+ try {
+ if (options.extraData)
+ for (var n in options.extraData)
+ extraInputs.push(
+ $('<input type="hidden" name="'+n+'" value="'+options.extraData[n]+'" />')
+ .appendTo(form)[0]);
+
+ // add iframe to doc and submit the form
+ $io.appendTo('body');
+ io.attachEvent ? io.attachEvent('onload', cb) : io.addEventListener('load', cb, false);
+ form.submit();
+ }
+ finally {
+ // reset attrs and remove "extra" input elements
+ $form.attr('action', a);
+ t ? $form.attr('target', t) : $form.removeAttr('target');
+ $(extraInputs).remove();
+ }
+ }, 10);
+
+ function cb() {
+ if (cbInvoked++) return;
+
+ io.detachEvent ? io.detachEvent('onload', cb) : io.removeEventListener('load', cb, false);
+
+ var ok = true;
+ try {
+ if (timedOut) throw 'timeout';
+ // extract the server response from the iframe
+ var data, doc;
+ doc = io.contentWindow ? io.contentWindow.document : io.contentDocument ? io.contentDocument : io.document;
+ xhr.responseText = doc.body ? doc.body.innerHTML : null;
+ xhr.responseXML = doc.XMLDocument ? doc.XMLDocument : doc;
+ xhr.getResponseHeader = function(header){
+ var headers = {'content-type': opts.dataType};
+ return headers[header];
+ };
+
+ if (opts.dataType == 'json' || opts.dataType == 'script') {
+ var ta = doc.getElementsByTagName('textarea')[0];
+ xhr.responseText = ta ? ta.value : xhr.responseText;
+ }
+ else if (opts.dataType == 'xml' && !xhr.responseXML && xhr.responseText != null) {
+ xhr.responseXML = toXml(xhr.responseText);
+ }
+ data = $.httpData(xhr, opts.dataType);
+ }
+ catch(e){
+ ok = false;
+ $.handleError(opts, xhr, 'error', e);
+ }
+
+ // ordering of these callbacks/triggers is odd, but that's how $.ajax does it
+ if (ok) {
+ opts.success(data, 'success');
+ if (g) $.event.trigger("ajaxSuccess", [xhr, opts]);
+ }
+ if (g) $.event.trigger("ajaxComplete", [xhr, opts]);
+ if (g && ! --$.active) $.event.trigger("ajaxStop");
+ if (opts.complete) opts.complete(xhr, ok ? 'success' : 'error');
+
+ // clean up
+ setTimeout(function() {
+ $io.remove();
+ xhr.responseXML = null;
+ }, 100);
+ };
+
+ function toXml(s, doc) {
+ if (window.ActiveXObject) {
+ doc = new ActiveXObject('Microsoft.XMLDOM');
+ doc.async = 'false';
+ doc.loadXML(s);
+ }
+ else
+ doc = (new DOMParser()).parseFromString(s, 'text/xml');
+ return (doc && doc.documentElement && doc.documentElement.tagName != 'parsererror') ? doc : null;
+ };
+ };
+};
+
+/**
+ * ajaxForm() provides a mechanism for fully automating form submission.
+ *
+ * The advantages of using this method instead of ajaxSubmit() are:
+ *
+ * 1: This method will include coordinates for <input type="image" /> elements (if the element
+ * is used to submit the form).
+ * 2. This method will include the submit element's name/value data (for the element that was
+ * used to submit the form).
+ * 3. This method binds the submit() method to the form for you.
+ *
+ * Note that for accurate x/y coordinates of image submit elements in all browsers
+ * you need to also use the "dimensions" plugin (this method will auto-detect its presence).
+ *
+ * The options argument for ajaxForm works exactly as it does for ajaxSubmit. ajaxForm merely
+ * passes the options argument along after properly binding events for submit elements and
+ * the form itself. See ajaxSubmit for a full description of the options argument.
+ *
+ *
+ * @example
+ * var options = {
+ * target: '#myTargetDiv'
+ * };
+ * $('#myForm').ajaxSForm(options);
+ * @desc Bind form's submit event so that 'myTargetDiv' is updated with the server response
+ * when the form is submitted.
+ *
+ *
+ * @example
+ * var options = {
+ * success: function(responseText) {
+ * alert(responseText);
+ * }
+ * };
+ * $('#myForm').ajaxSubmit(options);
+ * @desc Bind form's submit event so that server response is alerted after the form is submitted.
+ *
+ *
+ * @example
+ * var options = {
+ * beforeSubmit: function(formArray, jqForm) {
+ * if (formArray.length == 0) {
+ * alert('Please enter data.');
+ * return false;
+ * }
+ * }
+ * };
+ * $('#myForm').ajaxSubmit(options);
+ * @desc Bind form's submit event so that pre-submit callback is invoked before the form
+ * is submitted.
+ *
+ *
+ * @name ajaxForm
+ * @param options object literal containing options which control the form submission process
+ * @return jQuery
+ * @cat Plugins/Form
+ * @type jQuery
+ */
+$.fn.ajaxForm = function(options) {
+ return this.ajaxFormUnbind().bind('submit.form-plugin',function() {
+ $(this).ajaxSubmit(options);
+ return false;
+ }).each(function() {
+ // store options in hash
+ $(":submit,input:image", this).bind('click.form-plugin',function(e) {
+ var $form = this.form;
+ $form.clk = this;
+ if (this.type == 'image') {
+ if (e.offsetX != undefined) {
+ $form.clk_x = e.offsetX;
+ $form.clk_y = e.offsetY;
+ } else if (typeof $.fn.offset == 'function') { // try to use dimensions plugin
+ var offset = $(this).offset();
+ $form.clk_x = e.pageX - offset.left;
+ $form.clk_y = e.pageY - offset.top;
+ } else {
+ $form.clk_x = e.pageX - this.offsetLeft;
+ $form.clk_y = e.pageY - this.offsetTop;
+ }
+ }
+ // clear form vars
+ setTimeout(function() { $form.clk = $form.clk_x = $form.clk_y = null; }, 10);
+ });
+ });
+};
+
+
+/**
+ * ajaxFormUnbind unbinds the event handlers that were bound by ajaxForm
+ *
+ * @name ajaxFormUnbind
+ * @return jQuery
+ * @cat Plugins/Form
+ * @type jQuery
+ */
+$.fn.ajaxFormUnbind = function() {
+ this.unbind('submit.form-plugin');
+ return this.each(function() {
+ $(":submit,input:image", this).unbind('click.form-plugin');
+ });
+
+};
+
+/**
+ * formToArray() gathers form element data into an array of objects that can
+ * be passed to any of the following ajax functions: $.get, $.post, or load.
+ * Each object in the array has both a 'name' and 'value' property. An example of
+ * an array for a simple login form might be:
+ *
+ * [ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ]
+ *
+ * It is this array that is passed to pre-submit callback functions provided to the
+ * ajaxSubmit() and ajaxForm() methods.
+ *
+ * The semantic argument can be used to force form serialization in semantic order.
+ * This is normally true anyway, unless the form contains input elements of type='image'.
+ * If your form must be submitted with name/value pairs in semantic order and your form
+ * contains an input of type='image" then pass true for this arg, otherwise pass false
+ * (or nothing) to avoid the overhead for this logic.
+ *
+ * @example var data = $("#myForm").formToArray();
+ * $.post( "myscript.cgi", data );
+ * @desc Collect all the data from a form and submit it to the server.
+ *
+ * @name formToArray
+ * @param semantic true if serialization must maintain strict semantic ordering of elements (slower)
+ * @type Array<Object>
+ * @cat Plugins/Form
+ */
+$.fn.formToArray = function(semantic) {
+ var a = [];
+ if (this.length == 0) return a;
+
+ var form = this[0];
+ var els = semantic ? form.getElementsByTagName('*') : form.elements;
+ if (!els) return a;
+ for(var i=0, max=els.length; i < max; i++) {
+ var el = els[i];
+ var n = el.name;
+ if (!n) continue;
+
+ if (semantic && form.clk && el.type == "image") {
+ // handle image inputs on the fly when semantic == true
+ if(!el.disabled && form.clk == el)
+ a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
+ continue;
+ }
+
+ var v = $.fieldValue(el, true);
+ if (v && v.constructor == Array) {
+ for(var j=0, jmax=v.length; j < jmax; j++)
+ a.push({name: n, value: v[j]});
+ }
+ else if (v !== null && typeof v != 'undefined')
+ a.push({name: n, value: v});
+ }
+
+ if (!semantic && form.clk) {
+ // input type=='image' are not found in elements array! handle them here
+ var inputs = form.getElementsByTagName("input");
+ for(var i=0, max=inputs.length; i < max; i++) {
+ var input = inputs[i];
+ var n = input.name;
+ if(n && !input.disabled && input.type == "image" && form.clk == input)
+ a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
+ }
+ }
+ return a;
+};
+
+
+/**
+ * Serializes form data into a 'submittable' string. This method will return a string
+ * in the format: name1=value1&amp;name2=value2
+ *
+ * The semantic argument can be used to force form serialization in semantic order.
+ * If your form must be submitted with name/value pairs in semantic order then pass
+ * true for this arg, otherwise pass false (or nothing) to avoid the overhead for
+ * this logic (which can be significant for very large forms).
+ *
+ * @example var data = $("#myForm").formSerialize();
+ * $.ajax('POST', "myscript.cgi", data);
+ * @desc Collect all the data from a form into a single string
+ *
+ * @name formSerialize
+ * @param semantic true if serialization must maintain strict semantic ordering of elements (slower)
+ * @type String
+ * @cat Plugins/Form
+ */
+$.fn.formSerialize = function(semantic) {
+ //hand off to jQuery.param for proper encoding
+ return $.param(this.formToArray(semantic));
+};
+
+
+/**
+ * Serializes all field elements in the jQuery object into a query string.
+ * This method will return a string in the format: name1=value1&amp;name2=value2
+ *
+ * The successful argument controls whether or not serialization is limited to
+ * 'successful' controls (per http://www.w3.org/TR/html4/interact/forms.html#successful-controls).
+ * The default value of the successful argument is true.
+ *
+ * @example var data = $("input").fieldSerialize();
+ * @desc Collect the data from all successful input elements into a query string
+ *
+ * @example var data = $(":radio").fieldSerialize();
+ * @desc Collect the data from all successful radio input elements into a query string
+ *
+ * @example var data = $("#myForm :checkbox").fieldSerialize();
+ * @desc Collect the data from all successful checkbox input elements in myForm into a query string
+ *
+ * @example var data = $("#myForm :checkbox").fieldSerialize(false);
+ * @desc Collect the data from all checkbox elements in myForm (even the unchecked ones) into a query string
+ *
+ * @example var data = $(":input").fieldSerialize();
+ * @desc Collect the data from all successful input, select, textarea and button elements into a query string
+ *
+ * @name fieldSerialize
+ * @param successful true if only successful controls should be serialized (default is true)
+ * @type String
+ * @cat Plugins/Form
+ */
+$.fn.fieldSerialize = function(successful) {
+ var a = [];
+ this.each(function() {
+ var n = this.name;
+ if (!n) return;
+ var v = $.fieldValue(this, successful);
+ if (v && v.constructor == Array) {
+ for (var i=0,max=v.length; i < max; i++)
+ a.push({name: n, value: v[i]});
+ }
+ else if (v !== null && typeof v != 'undefined')
+ a.push({name: this.name, value: v});
+ });
+ //hand off to jQuery.param for proper encoding
+ return $.param(a);
+};
+
+
+/**
+ * Returns the value(s) of the element in the matched set. For example, consider the following form:
+ *
+ * <form><fieldset>
+ * <input name="A" type="text" />
+ * <input name="A" type="text" />
+ * <input name="B" type="checkbox" value="B1" />
+ * <input name="B" type="checkbox" value="B2"/>
+ * <input name="C" type="radio" value="C1" />
+ * <input name="C" type="radio" value="C2" />
+ * </fieldset></form>
+ *
+ * var v = $(':text').fieldValue();
+ * // if no values are entered into the text inputs
+ * v == ['','']
+ * // if values entered into the text inputs are 'foo' and 'bar'
+ * v == ['foo','bar']
+ *
+ * var v = $(':checkbox').fieldValue();
+ * // if neither checkbox is checked
+ * v === undefined
+ * // if both checkboxes are checked
+ * v == ['B1', 'B2']
+ *
+ * var v = $(':radio').fieldValue();
+ * // if neither radio is checked
+ * v === undefined
+ * // if first radio is checked
+ * v == ['C1']
+ *
+ * The successful argument controls whether or not the field element must be 'successful'
+ * (per http://www.w3.org/TR/html4/interact/forms.html#successful-controls).
+ * The default value of the successful argument is true. If this value is false the value(s)
+ * for each element is returned.
+ *
+ * Note: This method *always* returns an array. If no valid value can be determined the
+ * array will be empty, otherwise it will contain one or more values.
+ *
+ * @example var data = $("#myPasswordElement").fieldValue();
+ * alert(data[0]);
+ * @desc Alerts the current value of the myPasswordElement element
+ *
+ * @example var data = $("#myForm :input").fieldValue();
+ * @desc Get the value(s) of the form elements in myForm
+ *
+ * @example var data = $("#myForm :checkbox").fieldValue();
+ * @desc Get the value(s) for the successful checkbox element(s) in the jQuery object.
+ *
+ * @example var data = $("#mySingleSelect").fieldValue();
+ * @desc Get the value(s) of the select control
+ *
+ * @example var data = $(':text').fieldValue();
+ * @desc Get the value(s) of the text input or textarea elements
+ *
+ * @example var data = $("#myMultiSelect").fieldValue();
+ * @desc Get the values for the select-multiple control
+ *
+ * @name fieldValue
+ * @param Boolean successful true if only the values for successful controls should be returned (default is true)
+ * @type Array<String>
+ * @cat Plugins/Form
+ */
+$.fn.fieldValue = function(successful) {
+ for (var val=[], i=0, max=this.length; i < max; i++) {
+ var el = this[i];
+ var v = $.fieldValue(el, successful);
+ if (v === null || typeof v == 'undefined' || (v.constructor == Array && !v.length))
+ continue;
+ v.constructor == Array ? $.merge(val, v) : val.push(v);
+ }
+ return val;
+};
+
+/**
+ * Returns the value of the field element.
+ *
+ * The successful argument controls whether or not the field element must be 'successful'
+ * (per http://www.w3.org/TR/html4/interact/forms.html#successful-controls).
+ * The default value of the successful argument is true. If the given element is not
+ * successful and the successful arg is not false then the returned value will be null.
+ *
+ * Note: If the successful flag is true (default) but the element is not successful, the return will be null
+ * Note: The value returned for a successful select-multiple element will always be an array.
+ * Note: If the element has no value the return value will be undefined.
+ *
+ * @example var data = jQuery.fieldValue($("#myPasswordElement")[0]);
+ * @desc Gets the current value of the myPasswordElement element
+ *
+ * @name fieldValue
+ * @param Element el The DOM element for which the value will be returned
+ * @param Boolean successful true if value returned must be for a successful controls (default is true)
+ * @type String or Array<String> or null or undefined
+ * @cat Plugins/Form
+ */
+$.fieldValue = function(el, successful) {
+ var n = el.name, t = el.type, tag = el.tagName.toLowerCase();
+ if (typeof successful == 'undefined') successful = true;
+
+ if (successful && (!n || el.disabled || t == 'reset' || t == 'button' ||
+ (t == 'checkbox' || t == 'radio') && !el.checked ||
+ (t == 'submit' || t == 'image') && el.form && el.form.clk != el ||
+ tag == 'select' && el.selectedIndex == -1))
+ return null;
+
+ if (tag == 'select') {
+ var index = el.selectedIndex;
+ if (index < 0) return null;
+ var a = [], ops = el.options;
+ var one = (t == 'select-one');
+ var max = (one ? index+1 : ops.length);
+ for(var i=(one ? index : 0); i < max; i++) {
+ var op = ops[i];
+ if (op.selected) {
+ // extra pain for IE...
+ var v = $.browser.msie && !(op.attributes['value'].specified) ? op.text : op.value;
+ if (one) return v;
+ a.push(v);
+ }
+ }
+ return a;
+ }
+ return el.value;
+};
+
+
+/**
+ * Clears the form data. Takes the following actions on the form's input fields:
+ * - input text fields will have their 'value' property set to the empty string
+ * - select elements will have their 'selectedIndex' property set to -1
+ * - checkbox and radio inputs will have their 'checked' property set to false
+ * - inputs of type submit, button, reset, and hidden will *not* be effected
+ * - button elements will *not* be effected
+ *
+ * @example $('form').clearForm();
+ * @desc Clears all forms on the page.
+ *
+ * @name clearForm
+ * @type jQuery
+ * @cat Plugins/Form
+ */
+$.fn.clearForm = function() {
+ return this.each(function() {
+ $('input,select,textarea', this).clearFields();
+ });
+};
+
+/**
+ * Clears the selected form elements. Takes the following actions on the matched elements:
+ * - input text fields will have their 'value' property set to the empty string
+ * - select elements will have their 'selectedIndex' property set to -1
+ * - checkbox and radio inputs will have their 'checked' property set to false
+ * - inputs of type submit, button, reset, and hidden will *not* be effected
+ * - button elements will *not* be effected
+ *
+ * @example $('.myInputs').clearFields();
+ * @desc Clears all inputs with class myInputs
+ *
+ * @name clearFields
+ * @type jQuery
+ * @cat Plugins/Form
+ */
+$.fn.clearFields = $.fn.clearInputs = function() {
+ return this.each(function() {
+ var t = this.type, tag = this.tagName.toLowerCase();
+ if (t == 'text' || t == 'password' || tag == 'textarea')
+ this.value = '';
+ else if (t == 'checkbox' || t == 'radio')
+ this.checked = false;
+ else if (tag == 'select')
+ this.selectedIndex = -1;
+ });
+};
+
+
+/**
+ * Resets the form data. Causes all form elements to be reset to their original value.
+ *
+ * @example $('form').resetForm();
+ * @desc Resets all forms on the page.
+ *
+ * @name resetForm
+ * @type jQuery
+ * @cat Plugins/Form
+ */
+$.fn.resetForm = function() {
+ return this.each(function() {
+ // guard against an input with the name of 'reset'
+ // note that IE reports the reset function as an 'object'
+ if (typeof this.reset == 'function' || (typeof this.reset == 'object' && !this.reset.nodeType))
+ this.reset();
+ });
+};
+
+
+/**
+ * Enables or disables any matching elements.
+ *
+ * @example $(':radio').enabled(false);
+ * @desc Disables all radio buttons
+ *
+ * @name select
+ * @type jQuery
+ * @cat Plugins/Form
+ */
+$.fn.enable = function(b) {
+ if (b == undefined) b = true;
+ return this.each(function() {
+ this.disabled = !b
+ });
+};
+
+/**
+ * Checks/unchecks any matching checkboxes or radio buttons and
+ * selects/deselects and matching option elements.
+ *
+ * @example $(':checkbox').select();
+ * @desc Checks all checkboxes
+ *
+ * @name select
+ * @type jQuery
+ * @cat Plugins/Form
+ */
+$.fn.select = function(select) {
+ if (select == undefined) select = true;
+ return this.each(function() {
+ var t = this.type;
+ if (t == 'checkbox' || t == 'radio')
+ this.checked = select;
+ else if (this.tagName.toLowerCase() == 'option') {
+ var $sel = $(this).parent('select');
+ if (select && $sel[0] && $sel[0].type == 'select-one') {
+ // deselect all other options
+ $sel.find('option').select(false);
+ }
+ this.selected = select;
+ }
+ });
+};
+
+})(jQuery);
137 src/ext/jquery.jcache.js
@@ -0,0 +1,137 @@
+/**
+ * jCache - A client cache plugin for jQuery
+ * Should come in handy when data needs to be cached in client to improve performance.
+ * Author: Phan Van An
+ * phoenixheart@gmail.com
+ * http://www.skidvn.com
+ * License : Read jQuery's license
+
+Usage:
+ 1. Include this plugin into your web document after jQuery:
+ <script type="text/javascript" src="js/jquery.jcache.js"></script>
+ 2. [OPTIONAL] Set the max cached item number, for example 20
+ $.jCache.maxSize = 20;
+ 3. Start playing around with it:
+ - Put an item into cache: $.jCache.setItem(theKey, the Value);
+ - Retrieve an item from cache: var theValue = $.jCache.getItem(theKey);
+ - ...
+
+Url: http://plugins.jquery.com/project/jCache
+ */
+(function (jQuery){
+ this.version = '(beta)(0.0.1)';
+
+ /**
+ * The maximum items this cache should hold.
+ * If the cache is going to be overload, oldest item will be deleted (FIFO).
+ * Since the cached object is retained inside browser's state,
+ * a too big value on a too big web apps may affect system memory.
+ * Default is 10.
+ */
+ this.maxSize = 10;
+
+ /**
+ * An array to keep track of the cache keys
+ */
+ this.keys = new Array();
+
+ /**
+ * Number of currently cached items
+ */
+ this.cache_length = 0;
+
+ /**
+ * An associated array to contain the cached items
+ */
+ this.items = new Array();
+
+ /*
+ * @desc Puts an item into the cache
+ *
+ * @param string Key of the item
+ * @param string Value of the item
+ * @return string Value of the item
+ */
+ this.setItem = function(pKey, pValue)
+ {
+ if (typeof(pValue) != 'undefined')
+ {
+ if (typeof(this.items[pKey]) == 'undefined')
+ {
+ this.cache_length++;
+ }
+
+ this.keys.push(pKey);
+ this.items[pKey] = pValue;
+
+ if (this.cache_length > this.maxSize)
+ {
+ this.removeOldestItem();
+ }
+ }
+
+ return pValue;
+ }
+
+ /*
+ * @desc Removes an item from the cache using its key
+ * @param string Key of the item
+ */
+ this.removeItem = function(pKey)
+ {
+ var tmp;
+ if (typeof(this.items[pKey]) != 'undefined')
+ {
+ this.cache_length--;
+ var tmp = this.items[pKey];
+ delete this.items[pKey];
+ }
+
+ return tmp;
+ }
+
+ /*
+ * @desc Retrieves an item from the cache by its key
+ *
+ * @param string Key of the item
+ * @return string Value of the item
+ */
+ this.getItem = function(pKey)
+ {
+ return this.items[pKey];
+ }
+
+ /*
+ * @desc Indicates if the cache has an item specified by its key
+ * @param string Key of the item
+ * @return boolean TRUE or FALSE
+ */
+ this.hasItem = function(pKey)
+ {
+ return typeof(this.items[pKey]) != 'undefined';
+ }
+
+ /**
+ * @desc Removes the oldest cached item from the cache
+ */
+ this.removeOldestItem = function()
+ {
+ this.removeItem(this.keys.shift());
+ }
+
+ /**
+ * @desc Clears the cache
+ * @return Number of items cleared
+ */
+ this.clear = function()
+ {
+ var tmp = this.cache_length;
+ this.keys = new Array();
+ this.cache_length = 0;
+ this.items = new Array();
+ return tmp;
+ }
+
+ jQuery.jCache = this;
+ return jQuery;
+})(jQuery);
3,408 src/ext/jquery.js
3,408 additions, 0 deletions not shown because the diff is too large. Please use a local Git client to view these changes.
250 src/ext/jquery.livequery.js
@@ -0,0 +1,250 @@
+/* Copyright (c) 2007 Brandon Aaron (brandon.aaron@gmail.com || http://brandonaaron.net)
+ * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
+ * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
+ *
+ * Version: @VERSION
+ * Requires jQuery 1.1.3+
+ * Docs: http://docs.jquery.com/Plugins/livequery
+ */
+
+(function($) {
+
+$.extend($.fn, {
+ livequery: function(type, fn, fn2) {
+ var self = this, q;
+
+ // Handle different call patterns
+ if ($.isFunction(type))
+ fn2 = fn, fn = type, type = undefined;
+
+ // See if Live Query already exists
+ $.each( $.livequery.queries, function(i, query) {
+ if ( self.selector == query.selector && self.context == query.context &&
+ type == query.type && (!fn || fn.$lqguid == query.fn.$lqguid) && (!fn2 || fn2.$lqguid == query.fn2.$lqguid) )
+ // Found the query, exit the each loop
+ return (q = query) && false;
+ });
+
+ // Create new Live Query if it wasn't found
+ q = q || new $.livequery(this.selector, this.context, type, fn, fn2);
+
+ // Make sure it is running
+ q.stopped = false;
+
+ // Run it
+ $.livequery.run( q.id );
+
+ // Contnue the chain
+ return this;
+ },
+
+ expire: function(type, fn, fn2) {
+ var self = this;
+
+ // Handle different call patterns
+ if ($.isFunction(type))
+ fn2 = fn, fn = type, type = undefined;
+
+ // Find the Live Query based on arguments and stop it
+ $.each( $.livequery.queries, function(i, query) {
+ if ( self.selector == query.selector && self.context == query.context &&
+ (!type || type == query.type) && (!fn || fn.$lqguid == query.fn.$lqguid) && (!fn2 || fn2.$lqguid == query.fn2.$lqguid) && !this.stopped )
+ $.livequery.stop(query.id);
+ });
+
+ // Continue the chain
+ return this;
+ }
+});
+
+$.livequery = function(selector, context, type, fn, fn2) {
+ this.selector = selector;
+ this.context = context || document;
+ this.type = type;
+ this.fn = fn;
+ this.fn2 = fn2;
+ this.elements = [];
+ this.stopped = false;
+
+ // The id is the index of the Live Query in $.livequery.queries
+ this.id = $.livequery.queries.push(this)-1;
+
+ // Mark the functions for matching later on
+ fn.$lqguid = fn.$lqguid || $.livequery.guid++;
+ if (fn2) fn2.$lqguid = fn2.$lqguid || $.livequery.guid++;
+
+ // Return the Live Query
+ return this;
+};
+
+$.livequery.prototype = {
+ stop: function() {
+ var query = this;
+
+ if ( this.type )
+ // Unbind all bound events
+ this.elements.unbind(this.type, this.fn);
+ else if (this.fn2)
+ // Call the second function for all matched elements
+ this.elements.each(function(i, el) {
+ query.fn2.apply(el);
+ });
+
+ // Clear out matched elements
+ this.elements = [];
+
+ // Stop the Live Query from running until restarted
+ this.stopped = true;
+ },
+
+ run: function() {
+ // Short-circuit if stopped
+ if ( this.stopped ) return;
+ var query = this;
+
+ var oEls = this.elements,
+ els = $(this.selector, this.context),
+ nEls = els.not(oEls);
+
+ // Set elements to the latest set of matched elements
+ this.elements = els;
+
+ if (this.type) {
+ // Bind events to newly matched elements
+ nEls.bind(this.type, this.fn);
+
+ // Unbind events to elements no longer matched
+ if (oEls.length > 0)
+ $.each(oEls, function(i, el) {
+ if ( $.inArray(el, els) < 0 )
+ $.event.remove(el, query.type, query.fn);
+ });
+ }
+ else {
+ // Call the first function for newly matched elements
+ nEls.each(function() {
+ query.fn.apply(this);
+ });
+
+ // Call the second function for elements no longer matched
+ if ( this.fn2 && oEls.length > 0 )
+ $.each(oEls, function(i, el) {
+ if ( $.inArray(el, els) < 0 )
+ query.fn2.apply(el);
+ });
+ }
+ }
+};
+
+$.extend($.livequery, {
+ guid: 0,
+ queries: [],
+ queue: [],
+ running: false,
+ timeout: null,
+
+ checkQueue: function() {
+ if ( $.livequery.running && $.livequery.queue.length ) {
+ var length = $.livequery.queue.length;
+ // Run each Live Query currently in the queue
+ while ( length-- )
+ $.livequery.queries[ $.livequery.queue.shift() ].run();
+ }
+ },
+
+ pause: function() {
+ // Don't run anymore Live Queries until restarted
+ $.livequery.running = false;
+ },
+
+ play: function() {
+ // Restart Live Queries
+ $.livequery.running = true;
+ // Request a run of the Live Queries
+ $.livequery.run();
+ },
+
+ registerPlugin: function() {
+ $.each( arguments, function(i,n) {
+ // Short-circuit if the method doesn't exist
+ if (!$.fn[n]) return;
+
+ // Save a reference to the original method
+ var old = $.fn[n];
+
+ // Create a new method
+ $.fn[n] = function() {
+ // Call the original method
+ var r = old.apply(this, arguments);
+
+ // Request a run of the Live Queries
+ $.livequery.run();
+
+ // Return the original methods result
+ return r;
+ }
+ });
+ },
+
+ run: function(id) {
+ if (id != undefined) {
+ // Put the particular Live Query in the queue if it doesn't already exist
+ if ( $.inArray(id, $.livequery.queue) < 0 )
+ $.livequery.queue.push( id );
+ }
+ else
+ // Put each Live Query in the queue if it doesn't already exist
+ $.each( $.livequery.queries, function(id) {
+ if ( $.inArray(id, $.livequery.queue) < 0 )
+ $.livequery.queue.push( id );
+ });
+
+ // Clear timeout if it already exists
+ if ($.livequery.timeout) clearTimeout($.livequery.timeout);
+ // Create a timeout to check the queue and actually run the Live Queries
+ $.livequery.timeout = setTimeout($.livequery.checkQueue, 20);
+ },
+
+ stop: function(id) {
+ if (id != undefined)
+ // Stop are particular Live Query
+ $.livequery.queries[ id ].stop();
+ else
+ // Stop all Live Queries
+ $.each( $.livequery.queries, function(id) {
+ $.livequery.queries[ id ].stop();
+ });
+ }
+});
+
+// Register core DOM manipulation methods
+$.livequery.registerPlugin('append', 'prepend', 'after', 'before', 'wrap', 'attr', 'removeAttr', 'addClass', 'removeClass', 'toggleClass', 'empty', 'remove');
+
+// Run Live Queries when the Document is ready
+$(function() { $.livequery.play(); });
+
+
+// Save a reference to the original init method
+var init = $.prototype.init;
+
+// Create a new init method that exposes two new properties: selector and context
+$.prototype.init = function(a,c) {
+ // Call the original init and save the result
+ var r = init.apply(this, arguments);
+
+ // Copy over properties if they exist already
+ if (a && a.selector)
+ r.context = a.context, r.selector = a.selector;
+
+ // Set properties
+ if ( typeof a == 'string' )
+ r.context = c || document, r.selector = a;
+
+ // Return the result
+ return r;
+};
+
+// Give the init function the jQuery prototype for later instantiation (needed after Rev 4091)
+$.prototype.init.prototype = $.prototype;
+
+})(jQuery);
1 src/ext/jquery.template.js
@@ -0,0 +1 @@
+/**
7 src/github_badges.js
@@ -0,0 +1,7 @@
+<%= include 'HEADER' %>
+
+var GithubBadges = {
+ Version: '<%= APP_VERSION %>',
+};
+
+<%= include 'some_library_from_src_folder.js' %>
36 src/user_badge.js
@@ -0,0 +1,36 @@
+var GitHubBadge = GitHubBadge || {};
+GitHubBadge.buildUserBadge = function() {
+ var container = '#' + (arguments[0] || 'github-badge');
+ (function($){
+ $(document).ready(function() {
+ $(container).empty();
+ $(container).buildHeader();
+ $(container).buildBody();
+ $(container).buildFooter();
+ });
+ })(jQuery);
+};
+
+(function($){
+ $.fn.buildBody = function() {
+ this.append($(
+ "<div class='body'>"
+ + "</div>"
+ ));
+ };
+
+ $.fn.buildHeader = function() {
+ this.append($(
+ "<div class='header'>My projects</div>"
+ ));
+ };
+
+ $.fn.buildFooter = function() {
+ this.append($(
+ "<div class='footer'>"
+ + "Powered by <a href='http://github.com'>GitHub</a> | "
+ + "Written by <a href='http://drnicwilliams.com'>Dr Nic</a>"
+ + "</div>"
+ ));
+ };
+})(jQuery);
29 tasks/deploy.rake
@@ -0,0 +1,29 @@
+desc 'Package and upload the release to rubyforge.'
+task :release => [:clean, :dist, :package] do |t|
+ require 'rubyforge'
+ version = APP_VERSION
+ name = APP_NAME
+ rubyforge_name = RUBYFORGE_PROJECT
+ v = ENV["VERSION"] or abort "Must supply VERSION=x.y.z"
+ abort "Versions don't match #{v} vs #{version}" if v != version
+ pkg = "pkg/#{name}-#{version}"
+
+ if $DEBUG then
+ puts "release_id = rf.add_release #{rubyforge_name.inspect}, #{name.inspect}, #{version.inspect}, \"#{pkg}.tgz\""
+ puts "rf.add_file #{rubyforge_name.inspect}, #{name.inspect}, release_id, \"#{pkg}.gem\""
+ end
+
+ rf = RubyForge.new
+ puts "Logging in"
+ rf.login
+
+ c = rf.userconfig
+ c["release_notes"] = APP_DESCRIPTION if Object.const_defined?("APP_DESCRIPTION")
+ c["release_changes"] = APP_CHANGES if Object.const_defined?("APP_CHANGES")
+ c["preformatted"] = true
+
+ files = ["#{pkg}.tar.gz", "dist/#{name}-#{version}.js"].compact
+
+ puts "Releasing #{name} v. #{version}"
+ rf.add_release rubyforge_name, name, version, *files
+end
7 tasks/environment.rake
@@ -0,0 +1,7 @@
+task :ruby_env do
+ RUBY_APP = if RUBY_PLATFORM =~ /java/
+ "jruby"
+ else
+ "ruby"
+ end unless defined? RUBY_APP
+end
45 tasks/javascript_test_autotest_tasks.rake
@@ -0,0 +1,45 @@
+TEST_CHANGES_SINCE = Time.now - 600
+
+namespace :test do
+ namespace :recent do
+ desc "Open recently modified files into browser"
+ task :javascript do
+ require 'rubygems'
+ gem 'activesupport'
+ require 'active_support'
+
+ since = TEST_CHANGES_SINCE
+ touched = FileList[
+ 'test/unit/*_test.html',
+ 'src/*.js'].select { |path| File.mtime(path) > since }
+ next if touched.blank?
+
+ gem 'newjs'
+ require 'newjs'
+ require 'newjs/autotest'
+
+ touched.each do |file|
+ if file =~ /\/([^\/]+)\.js$/
+ file = "test/unit/#{$1}_test.html"
+ end
+ file = "#{APP_ROOT}/#{file}"
+ unless File.exists?(file)
+ # puts "Notice: Test file does not exist: #{file}"
+ next
+ end
+ puts "Launching test: #{file}"
+ browsers = JavascriptTestAutotest::Config.get :browsers
+ if browsers.blank?
+ puts "WARNING: No browsers setup in config/javascript_test_autotest.yml"
+ next
+ end
+ browsers.each_pair do |name, path|
+ browser = JavascriptTestAutotest::Browser.browser(name, path)
+ browser.setup
+ browser.visit(file)
+ browser.teardown
+ end
+ end
+ end
+ end
+end
964 test/assets/jsunittest.js
@@ -0,0 +1,964 @@
+/* Jsunittest, version 0.6.1
+ * (c) 2008 Dr Nic Williams
+ *
+ * Jsunittest is freely distributable under
+ * the terms of an MIT-style license.
+ * For details, see the web site: http://jsunittest.rubyforge.org
+ *
+ *--------------------------------------------------------------------------*/
+
+var JsUnitTest = {
+ Version: '0.6.1',
+};
+
+var DrNicTest = {
+ Unit: {},
+ inspect: function(object) {
+ try {
+ if (typeof object == "undefined") return 'undefined';
+ if (object === null) return 'null';
+ if (typeof object == "string") {
+ var useDoubleQuotes = arguments[1];
+ var escapedString = this.gsub(object, /[\x00-\x1f\\]/, function(match) {
+ var character = String.specialChar[match[0]];
+ return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16);
+ });
+ if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"';
+ return "'" + escapedString.replace(/'/g, '\\\'') + "'";
+ };
+ return String(object);
+ } catch (e) {
+ if (e instanceof RangeError) return '...';
+ throw e;
+ }
+ },
+ $: function(element) {
+ if (arguments.length > 1) {
+ for (var i = 0, elements = [], length = arguments.length; i < length; i++)
+ elements.push(this.$(arguments[i]));
+ return elements;
+ }
+ if (typeof element == "string")
+ element = document.getElementById(element);
+ return element;
+ },
+ gsub: function(source, pattern, replacement) {
+ var result = '', match;
+ replacement = arguments.callee.prepareReplacement(replacement);
+
+ while (source.length > 0) {
+ if (match = source.match(pattern)) {
+ result += source.slice(0, match.index);
+ result += DrNicTest.String.interpret(replacement(match));
+ source = source.slice(match.index + match[0].length);
+ } else {
+ result += source, source = '';
+ }
+ }
+ return result;
+ },
+ scan: function(source, pattern, iterator) {
+ this.gsub(source, pattern, iterator);
+ return String(source);
+ },
+ escapeHTML: function(data) {
+ return data.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
+ },
+ arrayfromargs: function(args) {
+ var myarray = new Array();
+ var i;
+
+ for (i=0;i<args.length;i++)
+ myarray[i] = args[i];
+
+ return myarray;
+ },
+ hashToSortedArray: function(hash) {
+ var results = [];
+ for (key in hash) {
+ results.push([key, hash[key]]);
+ }
+ return results.sort();
+ },
+ flattenArray: function(array) {
+ var results = arguments[1] || [];
+ for (var i=0; i < array.length; i++) {
+ var object = array[i];
+ if (object != null && typeof object == "object" &&
+ 'splice' in object && 'join' in object) {
+ this.flattenArray(object, results);
+ } else {
+ results.push(object);
+ }
+ };
+ return results;
+ },
+ selectorMatch: function(expression, element) {
+ var tokens = [];
+ var patterns = {
+ // combinators must be listed first
+ // (and descendant needs to be last combinator)
+ laterSibling: /^\s*~\s*/,
+ child: /^\s*>\s*/,
+ adjacent: /^\s*\+\s*/,
+ descendant: /^\s/,
+
+ // selectors follow
+ tagName: /^\s*(\*|[\w\-]+)(\b|$)?/,
+ id: /^#([