diff --git a/.gitignore b/.gitignore index 2460a37a8..d214bf775 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ .DS_Store pkg test/unit/tmp/* +test/unit/browsers.yml doc -tmp \ No newline at end of file +tmp +*.pdoc.yaml \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 00dca5233..0bce64053 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "vendor/unittest_js"] - path = vendor/unittest_js - url = git://github.com/tobie/unittest_js.git [submodule "vendor/caja_builder"] path = vendor/caja_builder url = git://github.com/tobie/unittest_js_caja_builder.git @@ -11,6 +8,13 @@ path = vendor/sprockets url = git://github.com/sstephenson/sprockets.git -[submodule "vendor/sizzle"] - path = vendor/sizzle - url = git://github.com/jeresig/sizzle.git + +[submodule "vendor/nwmatcher/repository"] + path = vendor/nwmatcher/repository + url = git://github.com/dperini/nwmatcher.git +[submodule "vendor/sizzle/repository"] + path = vendor/sizzle/repository + url = git://github.com/jquery/sizzle.git +[submodule "vendor/slick/repository"] + path = vendor/slick/repository + url = git://github.com/mootools/slick.git diff --git a/CHANGELOG b/CHANGELOG index f5df87f42..796e90876 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,116 @@ +----------------------------------------------------------------------------- +NOTE: This changelog is no longer updated. Consult the commit history for a +list of changes from version to version. +----------------------------------------------------------------------------- + +*1.7* (November 16, 2010) + +* Ensure `Element#update` works with string content that includes a LINK tag in Internet Explorer. [#264 state:resolved] (Tobias H. Michaelsen, Andrew Dupont) + +* Treat a 304 HTTP status as a successful response. [#331 state:resolved] (Kenneth Kin Lum, Andrew Dupont) + +* Handle sparse arrays properly in `Array#_each` to match behavior with browsers' built-in `Array#forEach` (and ES5). [#790 state:resolved] (Andriy Tyurnikov, Yaffle, Andrew Dupont) + +* Make `Event.extend` work with legacy IE events in IE 9. (Andrew Dupont) + +* Stop appending `&_=` to the parameters for non-GET Ajax requests in Safari. We no longer support any version of Safari for which this is necessary. [#327 state:resolved] (John-David Dalton, Andrew Dupont) + +* Ensure `Form.focusFirstElement` doesn't raise an exception on forms with no fields. [#341 state:resolved] (achernin, Andrew Dupont) + +* Define a `relatedTarget` property on extended mouseenter/mouseleave events in IE's legacy event system. [#708 state:resolved] (Walter Smith, Tobie Langel, Andrew Dupont) + +* Fix odd behavior with `new Element('select')` in IE6-7. [#480 state:resolved] (Bruce Harris, kangax, Andrew Dupont) + +* Extend BUTTON elements with everything defined in Form.Element.Methods. Ensure BUTTON elements are traversed in Form.getElements and serialized in Form.serialize. (Luis Gomez, Samuel Lebeau, kangax, Andrew Dupont) + +* Ensure Object.isFunction returns `false` for RegExp objects. [#661 state:resolved] (James, kangax, Andrew Dupont) + +* Revert Opera-specific behavior for calling Element#getStyle with (left|right|top|bottom). [#268 state:resolved] (kangax, Andrew Dupont) + +* Fix issue with Event#isMiddleClick and #isRightClick on Safari and Chrome. [#731 state:resolved] (Arthur Schreiber) + +* Fix issue where an Ajax request in IE sometimes returns 1223 instead of 204 as the status code. [#129 state:resolved] (adevadeh, gordyt, Andrew Dupont) + +* Add Object.isDate. [#443 state:resolved] (Nesterenko Dmitry, kangax, Samuel Lebeau, Andrew Dupont) + +* Handle cases where `document` or `document.documentElement` is passed into Element#getOffsetParent. Fixes IE errors with many layout/positioning methods. [#90 state:resolved] (Padraig Kennedy, Andrew Dupont) + +* Wrap `element` in `$` for Element#cumulativeOffset, #viewportOffset, #positionedOffset, and #getOffsetParent. [#782 state:resolved] (Radoslav Stankov, Andrew Dupont) + +*1.7_rc3* (October 11, 2010) + +* Feature detect IE's legacy event system, since IE9 now supports DOM L2 Events. (Michael Sciacqua, kangax, Andrew Dupont) + +* Fix an issue in IE9 beta where the `value` attribute of an element would not get set. (Andrew Dupont) + +* Move all offset methods to layout.js, eliminating some code repetition. + +* Remove unit test for passing a DOM node into `Object.toJSON`. To continue to support this use case would require bypassing IE's native JSON implementation. (Tobie Langel, Thomas Fuchs, Samuel Lebeau, Andrew Dupont) + +* Ensure hidden absolutely-positioned elements are sized correctly when they have no explicit width set. [#1084 state:resolved] (Viktor Kojouharov, Andrew Dupont) + +* Change custom events implementation to use the `onlosecapture` event instead of the `onfilterchange` event for non-bubbling custom events in Internet Explorer. (John-David Dalton, Andrew Dupont) + +* Revert `Element.getHeight` and `Element.getWidth` to their previous behavior in order to ensure backward-compatibility. (Sam Stephenson, Andrew Dupont) + +* Fix accidental declaration of `purgeElement` as a global function. [#1089 state:resolved] (Viktor Kojouharov, Andrew Dupont) + +* Ensure `Element.Layout` gives accurate measurements for percentages on all elements. Fix inaccurate measurements on `position: fixed` elements with percentages. [#1040 state:resolved] (Dan Popescu, Riccardo De Agostini, Andrew Dupont) + +* Fix issue with retrieving border-box-(height|width) of hidden elements. [#1035 state:resolved] (Francois-Pierre Bouchard, Andrew Dupont) + +* Ensure we clean up after ourselves for all `width` and `height` computations in `Element.Layout`. (Sam Stephenson, Andrew Dupont) + +*1.7_rc2* (May 12, 2010) + +* Remove redefinition of `Element#cumulativeOffset` when `getBoundingClientRect` is present, as it seems to give inaccurate results. (Andrew Dupont) + +* Optimize element purging. (RStankov, Andrew Dupont) + +* Add `Element.purge` for cleaning up event listeners and element storage keys on elements that will be removed from the page. Make `Element.update` perform similar cleanup automatically. (Andrew Dupont, Tobie Langel) + +* The second argument to Event.Handler callbacks is the target element when no selector is present, or the matching element when a selector is present. Callbacks are always bound to the original element. (sam) + +* Fix issue where `Element.Layout#get` would fail to interpret negative pixel values. (Sebastien Gruhier, Andrew Dupont) + +* Fix bugs in layout.js. Add tests for `Element.Layout#toCSS`, `#toObject`, and `#toHash`. (RStankov, Andrew Dupont) + +* Add `Element.Layout#toObject` and `Element.Layout.toHash`. (Andrew Dupont) + +* Make `Element.Layout#toCSS` return camelized property names, as expected by `Element.setStyle`. [#1021 state:resolved] (njakobsen, Andrew Dupont) + +*1.7_rc1* (April 1, 2010) + +* Add an `Event.Handler` class, plus `Event.on` and `Element#on` methods, for simplified event delegation. (sam, Tobie Langel, Andrew Dupont) + +* Make `Element.Layout` properly interpret computed non-integer pixel values. (e.g., Firefox can report "12.5px" as a computed style value.) (henrymazza) + +* Fix deprecated Selector.matchElements. (Tobie Langel) + +* Make Object.keys ES5 compliant. (Tobie Langel) + +* Make Prototype's JSON implementation EcmaScript 5 compliant. [#453 state:resolved] (Tobie Langel) + +* Also detect embedded (UIWebView) mobile Safari. (Thomas Fuchs) + +* Avoid object creation and an unnecessary function call in `Class#addMethods`, when working around JScript DontEnum bug. Replace with feature test and a simple boolean check at runtime. (kangax) + +* Optimize Element#immediateDescendants. (kangax, Tobie Langel) + +* Remove unnecessary function object creation and `Number#times` in `Element._getContentFromAnonymousElement`. (kangax) + +* Eliminate runtime forking and long method lookup in `Element.hasAttribute`. (kangax) + +* Remove redundant ternary. (kangax) + +* Avoid repeating declaration statements where it makes sense, for slightly better runtime performance and minification. (kangax) + +* Make `Event.stopObserving` return element in all cases. [#810 state:resolved] (Yaffle, Tobie Langel) + +* String#startsWith, String#endsWith performance optimization (Yaffle, Tobie Langel, kangax) + +* Rewrite String#camelize using String#replace with a replacement function (Phred, John-David Dalton, Samuel Lebeau, kangax) + *1.6.1* (August 24, 2009) * Avoid triggering a warning when Java is disabled in IE8. [#668 state:resolved] (orv, kangax, Andrew Dupont, Tobie Langel) diff --git a/LICENSE b/LICENSE index 61e491823..05789cf01 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2005-2008 Sam Stephenson +Copyright (c) 2005-2010 Sam Stephenson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.markdown b/README.markdown index ec7cabe22..afc7d0fdc 100644 --- a/README.markdown +++ b/README.markdown @@ -38,12 +38,12 @@ the `src/` directory. To build Prototype, you'll need: * Rake--Ruby Make () * RDoc, if your Ruby distribution does not include it -From the root Prototype directory, +From the root Prototype directory: * `rake dist` will preprocess the Prototype source using Sprockets and - generate the composite `dist/prototype.js`. + generate the composite `dist/prototype.js` * `rake package` will create a distribution tarball in the - `pkg/` directory. + `pkg/` directory Contributing to Prototype ------------------------- @@ -60,4 +60,4 @@ Find out how to contribute: . Documentation ------------- -Please see the online Prototype API: . \ No newline at end of file +Please see the online Prototype API: . diff --git a/Rakefile b/Rakefile index cff99f84d..a0a795150 100755 --- a/Rakefile +++ b/Rakefile @@ -1,21 +1,31 @@ require 'rake' require 'rake/packagetask' +require 'rbconfig' require 'yaml' module PrototypeHelper + extend Rake::DSL + ROOT_DIR = File.expand_path(File.dirname(__FILE__)) SRC_DIR = File.join(ROOT_DIR, 'src') DIST_DIR = File.join(ROOT_DIR, 'dist') DOC_DIR = File.join(ROOT_DIR, 'doc') TEMPLATES_DIR = File.join(ROOT_DIR, 'templates') PKG_DIR = File.join(ROOT_DIR, 'pkg') - SIZZLE_DIR = File.join(ROOT_DIR, 'vendor', 'sizzle') TEST_DIR = File.join(ROOT_DIR, 'test') TEST_UNIT_DIR = File.join(TEST_DIR, 'unit') TMP_DIR = File.join(TEST_UNIT_DIR, 'tmp') VERSION = YAML.load(IO.read(File.join(SRC_DIR, 'constants.yml')))['PROTOTYPE_VERSION'] - %w[sprockets pdoc unittest_js caja_builder sizzle].each do |name| + DEFAULT_SELECTOR_ENGINE = 'sizzle' + + host = RbConfig::CONFIG['host'] + IS_WINDOWS = host.include?('mswin') || host.include?('mingw32') + + # Possible options for PDoc syntax highlighting, in order of preference. + SYNTAX_HIGHLIGHTERS = [:pygments, :coderay, :none] + + %w[sprockets pdoc unittest_js caja_builder].each do |name| $:.unshift File.join(PrototypeHelper::ROOT_DIR, 'vendor', name, 'lib') end @@ -23,94 +33,194 @@ module PrototypeHelper begin `git --version` return true - rescue Error => e + rescue Error return false end end - + def self.require_git return if has_git? puts "\nPrototype requires Git in order to load its dependencies." puts "\nMake sure you've got Git installed and in your path." puts "\nFor more information, visit:\n\n" - puts " http://book.git-scm.com/2_installing_git.html" + puts " http://git-scm.com/book/en/v2/Getting-Started-Installing-Git" exit end - - def self.sprocketize(path, source, destination = nil, strip_comments = true) + + def self.sprocketize(options = {}) + options = { + :destination => File.join(DIST_DIR, options[:source]), + :strip_comments => true + }.merge(options) + require_sprockets - require_sizzle + load_path = [SRC_DIR] + + if selector_path = get_selector_engine(options[:selector_engine]) + load_path << selector_path + end + secretary = Sprockets::Secretary.new( - :root => File.join(ROOT_DIR, path), - :load_path => [SRC_DIR, SIZZLE_DIR], - :source_files => [source], - :strip_comments => strip_comments + :root => File.join(ROOT_DIR, options[:path]), + :load_path => load_path, + :source_files => [options[:source]], + :strip_comments => options[:strip_comments] ) - - destination = File.join(DIST_DIR, source) unless destination - secretary.concatenation.save_to(destination) + + secretary.concatenation.save_to(options[:destination]) end - + def self.build_doc_for(file) - mkdir_p TMP_DIR - temp_path = File.join(TMP_DIR, "prototype.temp.js") - sprocketize('src', file, temp_path, false) - rm_rf DOC_DIR - - PDoc::Runner.new(temp_path, { - :output => DOC_DIR, - :templates => File.join(TEMPLATES_DIR, "html"), - :index_page => 'README.markdown' - }).run - - rm_rf temp_path - end - + rm_rf(DOC_DIR) + mkdir_p(DOC_DIR) + hash = current_head + index_header = < + + Prototype JavaScript Framework API + + +EOF + PDoc.run({ + :source_files => Dir[File.join('src', 'prototype', '**', '*.js')], + :destination => DOC_DIR, + :index_page => 'README.markdown', + :syntax_highlighter => syntax_highlighter, + :markdown_parser => :bluecloth, + :src_code_text => "View source on GitHub →", + :src_code_href => proc { |obj| + "https://github.com/sstephenson/prototype/blob/#{hash}/#{obj.file}#L#{obj.line_number}" + }, + :pretty_urls => false, + :bust_cache => false, + :name => 'Prototype JavaScript Framework', + :short_name => 'Prototype', + :home_url => 'http://prototypejs.org', + :version => PrototypeHelper::VERSION, + :index_header => index_header, + :footer => 'This work is licensed under a Creative Commons Attribution-Share Alike 3.0 Unported License.', + :assets => 'doc_assets' + }) + end + + def self.require_package(name) + begin + require name + rescue LoadError + puts "You need the #{name} package. Try installing it with:\n" + puts " $ gem install #{name}" + exit + end + end + + def self.require_phantomjs + cmd = IS_WINDOWS ? "phantomjs.cmd -v" : "phantomjs -v > /dev/null 2>&1" + success = system(cmd) + if !success + puts "\nYou need phantomjs installed to run this task. Find out how at:" + puts " http://phantomjs.org/download.html" + exit + end + end + + def self.syntax_highlighter + if ENV['SYNTAX_HIGHLIGHTER'] + highlighter = ENV['SYNTAX_HIGHLIGHTER'].to_sym + require_highlighter(highlighter, true) + return highlighter + end + + SYNTAX_HIGHLIGHTERS.detect { |n| require_highlighter(n) } + end + + def self.require_highlighter(name, verbose=false) + case name + when :pygments + success = system("pygmentize -V > /dev/null") + if !success && verbose + puts "\nYou asked to use Pygments, but I can't find the 'pygmentize' binary." + puts "To install, visit:\n" + puts " http://pygments.org/docs/installation/\n\n" + exit + end + return success # (we have pygments) + when :coderay + begin + require 'coderay' + rescue LoadError + if verbose + puts "\nYou asked to use CodeRay, but I can't find the 'coderay' gem. Just run:\n\n" + puts " $ gem install coderay" + puts "\nand you should be all set.\n\n" + exit + end + return false + end + return true # (we have CodeRay) + when :none + return true + else + puts "\nYou asked to use a syntax highlighter I don't recognize." + puts "Valid options: #{SYNTAX_HIGHLIGHTERS.join(', ')}\n\n" + exit + end + end + def self.require_sprockets require_submodule('Sprockets', 'sprockets') end - + def self.require_pdoc require_submodule('PDoc', 'pdoc') end - + def self.require_unittest_js require_submodule('UnittestJS', 'unittest_js') end - + def self.require_caja_builder require_submodule('CajaBuilder', 'caja_builder') end - - def self.require_sizzle - if !File.exists?(File.join(SIZZLE_DIR, 'sizzle.js')) - exit unless get_submodule("Sizzle", "sizzle") + + def self.get_selector_engine(name) + return if !name + # If the submodule exists, we should use it. + submodule_path = File.join(ROOT_DIR, "vendor", name) + return submodule_path if File.exist?(File.join(submodule_path, "repository", ".git")) + return submodule_path if name === "legacy_selector" + + # If it doesn't exist, we should fetch it. + get_submodule('the required selector engine', "#{name}/repository") + unless File.exist?(submodule_path) + puts "The selector engine you required isn't available at vendor/#{name}.\n\n" + exit end end - + def self.get_submodule(name, path) require_git puts "\nYou seem to be missing #{name}. Obtaining it via git...\n\n" - + Kernel.system("git submodule init") return true if Kernel.system("git submodule update vendor/#{path}") - # If we got this far, something went wrong. puts "\nLooks like it didn't work. Try it manually:\n\n" puts " $ git submodule init" puts " $ git submodule update vendor/#{path}" false end - + def self.require_submodule(name, path) begin - require path + full_path = File.join(PrototypeHelper::ROOT_DIR, 'vendor', path, 'lib', path) + # We need to require the explicit version in the submodule. + require full_path rescue LoadError => e # Wait until we notice that a submodule is missing before we bother the # user about installing git. (Maybe they brought all the files over # from a different machine.) - missing_file = e.message.sub('no such file to load -- ', '') - if missing_file == path + missing_file = e.message.sub('no such file to load -- ', '').sub('cannot load such file -- ', '') + if missing_file == full_path # Missing a git submodule. retry if get_submodule(name, path) else @@ -122,21 +232,29 @@ module PrototypeHelper exit end end + + def self.current_head + `git show-ref --hash HEAD`.chomp[0..6] + end end task :default => [:dist, :dist_helper, :package, :clean_package_source] desc "Builds the distribution." task :dist do - PrototypeHelper.sprocketize("src", "prototype.js") + PrototypeHelper.sprocketize( + :path => 'src', + :source => 'prototype.js', + :selector_engine => ENV['SELECTOR_ENGINE'] || PrototypeHelper::DEFAULT_SELECTOR_ENGINE + ) end namespace :doc do desc "Builds the documentation." task :build => [:require] do PrototypeHelper.build_doc_for(ENV['SECTION'] ? "#{ENV['SECTION']}.js" : 'prototype.js') - end - + end + task :require do PrototypeHelper.require_pdoc end @@ -146,7 +264,7 @@ task :doc => ['doc:build'] desc "Builds the updating helper." task :dist_helper do - PrototypeHelper.sprocketize("ext/update_helper", "prototype_update_helper.js") + PrototypeHelper.sprocketize(:path => 'ext/update_helper', :source => 'prototype_update_helper.js') end Rake::PackageTask.new('prototype', PrototypeHelper::VERSION) do |package| @@ -165,83 +283,37 @@ task :clean_package_source do rm_rf File.join(PrototypeHelper::PKG_DIR, "prototype-#{PrototypeHelper::VERSION}") end -task :test => ['test:build', 'test:run'] +task :test => ['test:require', 'test:start'] namespace :test do - desc 'Runs all the JavaScript unit tests and collects the results' - task :run => [:require] do - testcases = ENV['TESTCASES'] - browsers_to_test = ENV['BROWSERS'] && ENV['BROWSERS'].split(',') - tests_to_run = ENV['TESTS'] && ENV['TESTS'].split(',') - runner = UnittestJS::WEBrickRunner::Runner.new(:test_dir => PrototypeHelper::TMP_DIR) - - Dir[File.join(PrototypeHelper::TMP_DIR, '*_test.html')].each do |file| - file = File.basename(file) - test = file.sub('_test.html', '') - unless tests_to_run && !tests_to_run.include?(test) - runner.add_test(file, testcases) - end - end - - UnittestJS::Browser::SUPPORTED.each do |browser| - unless browsers_to_test && !browsers_to_test.include?(browser) - runner.add_browser(browser.to_sym) - end - end - - trap('INT') { runner.teardown; exit } - runner.run - end - - task :build => [:clean, :dist] do - builder = UnittestJS::Builder::SuiteBuilder.new({ - :input_dir => PrototypeHelper::TEST_UNIT_DIR, - :assets_dir => PrototypeHelper::DIST_DIR - }) - selected_tests = (ENV['TESTS'] || '').split(',') - builder.collect(*selected_tests) - builder.render - end - - task :clean => [:require] do - UnittestJS::Builder.empty_dir!(PrototypeHelper::TMP_DIR) + desc 'Starts the test server.' + task :start => [:require] do + path_to_app = File.join(PrototypeHelper::ROOT_DIR, 'test', 'unit', 'server.rb') + require path_to_app + + puts "Starting unit test server..." + puts "Unit tests available at \n\n" + UnitTests.run! end - + task :require do - PrototypeHelper.require_unittest_js + PrototypeHelper.require_package('sinatra') end -end -task :test_units do - puts '"rake test_units" is deprecated. Please use "rake test" instead.' -end - -task :build_unit_tests do - puts '"rake test_units" is deprecated. Please use "rake test:build" instead.' -end - -task :clean_tmp do - puts '"rake clean_tmp" is deprecated. Please use "rake test:clean" instead.' -end + desc "Opens the test suite in several different browsers. (Does not start or stop the server; you should do that separately.)" + task :run => [:require] do + browsers, tests, grep = ENV['BROWSERS'], ENV['TESTS'], ENV['GREP'] + path_to_runner = File.join(PrototypeHelper::ROOT_DIR, 'test', 'unit', 'runner.rb') + require path_to_runner -namespace :caja do - task :test => ['test:build', 'test:run'] - - namespace :test do - task :run => ['rake:test:run'] - - task :build => [:require, 'rake:test:clean', :dist] do - builder = UnittestJS::CajaBuilder::SuiteBuilder.new({ - :input_dir => PrototypeHelper::TEST_UNIT_DIR, - :assets_dir => PrototypeHelper::DIST_DIR, - :whitelist_dir => File.join(PrototypeHelper::TEST_DIR, 'unit', 'caja_whitelists'), - :html_attrib_schema => 'html_attrib.json' - }) - selected_tests = (ENV['TESTS'] || '').split(',') - builder.collect(*selected_tests) - builder.render - end + Runner::run(browsers, tests, grep) end - task :require => ['rake:test:require'] do - PrototypeHelper.require_caja_builder + + desc "Runs the tests in PhantomJS. (Does not start or stop the server; you should do that separately.)" + task :phantom do + PrototypeHelper.require_phantomjs + tests, grep = ENV['TESTS'], ENV['GREP'] + url = "http://127.0.0.1:4567/test/#{tests}" + url << "?grep=#{grep}" if grep + system(%Q[phantomjs ./test/unit/phantomjs/mocha-phantomjs.js "#{url}"]) end -end \ No newline at end of file +end diff --git a/templates/html/assets/images/header-logo-small.png b/doc_assets/images/header-logo-small.png similarity index 100% rename from templates/html/assets/images/header-logo-small.png rename to doc_assets/images/header-logo-small.png diff --git a/templates/html/assets/images/header-stripe-small.png b/doc_assets/images/header-stripe-small.png similarity index 100% rename from templates/html/assets/images/header-stripe-small.png rename to doc_assets/images/header-stripe-small.png diff --git a/ext/update_helper/prototype_update_helper.html b/ext/update_helper/prototype_update_helper.html index b2d355de6..625911d5d 100644 --- a/ext/update_helper/prototype_update_helper.html +++ b/ext/update_helper/prototype_update_helper.html @@ -81,7 +81,12 @@

Prototype Unit test file

new Test.Unit.Runner({ testGetStack: function() { - this.assertMatch(/prototype_update_helper\.html:\d+\n$/, prototypeUpdateHelper.getStack()); + var stack = prototypeUpdateHelper.getStack(); + if (stack === '') { + this.info('UpdaterHelper#getStack is currently not supported on this browser.') + } else { + this.assertMatch(/prototype_update_helper\.html:\d+\n$/, prototypeUpdateHelper.getStack()); + } }, testDisplay: function() { @@ -280,6 +285,38 @@

Prototype Unit test file

this.assertNotNotified(); }, + testSelectorInstanceMethods: function() { + var selector = new Selector('div'); + this.assertWarnNotified('The Selector class has been deprecated. Please use the new Prototype.Selector API instead.'); + + selector.findElements(document); + this.assertWarnNotified('Selector#findElements has been deprecated. Please use the new Prototype.Selector API instead.'); + + selector.match(document.documentElement); + this.assertWarnNotified('Selector#match has been deprecated. Please use the new Prototype.Selector API instead.'); + + selector.toString(); + this.assertWarnNotified('Selector#toString has been deprecated. Please use the new Prototype.Selector API instead.'); + + selector.inspect(); + this.assertWarnNotified('Selector#inspect has been deprecated. Please use the new Prototype.Selector API instead.'); + }, + + testSelectorMatchElements: function() { + Selector.matchElements([], 'div'); + this.assertWarnNotified('Selector.matchElements has been deprecated. Please use the new Prototype.Selector API instead.'); + }, + + testSelectorFindElement: function() { + Selector.findElement([], 'div'); + this.assertWarnNotified('Selector.findElement has been deprecated. Please use the new Prototype.Selector API instead.'); + }, + + testSelectorFindChildElements: function() { + Selector.findChildElements(document, 'div'); + this.assertWarnNotified('Selector.findChildElements has been deprecated. Please use the new Prototype.Selector API instead.'); + }, + testLogDeprecationOption: function() { prototypeUpdateHelper.logLevel = UpdateHelper.Warn; var h = $H({ foo: 2 }); diff --git a/ext/update_helper/prototype_update_helper.js b/ext/update_helper/prototype_update_helper.js index e5107ca29..b0229667c 100644 --- a/ext/update_helper/prototype_update_helper.js +++ b/ext/update_helper/prototype_update_helper.js @@ -1,6 +1,6 @@ //= require "update_helper" -/* UpdateHelper for Prototype <%= PROTOTYPE_VERSION %> (c) 2008 Tobie Langel +/* UpdateHelper for Prototype <%= PROTOTYPE_VERSION %> (c) 2008-2009 Tobie Langel * * UpdateHelper for Prototype is freely distributable under the same * terms as Prototype (MIT-style license). @@ -17,7 +17,7 @@ * * This, for example, will prevent deprecation messages from being logged. * - * THIS SCRIPT WORKS IN FIREFOX ONLY + * THIS SCRIPT DOES NOT WORK IN INTERNET EXPLORER *--------------------------------------------------------------------------*/ var prototypeUpdateHelper = new UpdateHelper([ @@ -275,6 +275,62 @@ var prototypeUpdateHelper = new UpdateHelper([ message: 'The class API has been fully revised and now allows for mixins and inheritance.\n' + 'You can find more about it here: http://prototypejs.org/learn/class-inheritance', condition: function() { return !arguments.length } + }, + + { + methodName: 'initialize', + namespace: Selector.prototype, + message: 'The Selector class has been deprecated. Please use the new Prototype.Selector API instead.', + type: 'warn' + }, + + { + methodName: 'findElements', + namespace: Selector.prototype, + message: 'Selector#findElements has been deprecated. Please use the new Prototype.Selector API instead.', + type: 'warn' + }, + + { + methodName: 'match', + namespace: Selector.prototype, + message: 'Selector#match has been deprecated. Please use the new Prototype.Selector API instead.', + type: 'warn' + }, + + { + methodName: 'toString', + namespace: Selector.prototype, + message: 'Selector#toString has been deprecated. Please use the new Prototype.Selector API instead.', + type: 'warn' + }, + + { + methodName: 'inspect', + namespace: Selector.prototype, + message: 'Selector#inspect has been deprecated. Please use the new Prototype.Selector API instead.', + type: 'warn' + }, + + { + methodName: 'matchElements', + namespace: Selector, + message: 'Selector.matchElements has been deprecated. Please use the new Prototype.Selector API instead.', + type: 'warn' + }, + + { + methodName: 'findElement', + namespace: Selector, + message: 'Selector.findElement has been deprecated. Please use the new Prototype.Selector API instead.', + type: 'warn' + }, + + { + methodName: 'findChildElements', + namespace: Selector, + message: 'Selector.findChildElements has been deprecated. Please use the new Prototype.Selector API instead.', + type: 'warn' } ]); diff --git a/ext/update_helper/update_helper.js b/ext/update_helper/update_helper.js index 81a0ad082..2a4b7b6c4 100644 --- a/ext/update_helper/update_helper.js +++ b/ext/update_helper/update_helper.js @@ -1,4 +1,4 @@ -/* Update Helper (c) 2008 Tobie Langel +/* Update Helper (c) 2008-2009 Tobie Langel * * Requires Prototype >= 1.6.0 * @@ -54,9 +54,12 @@ var UpdateHelper = Class.create({ try { throw new Error("stack"); } catch(e) { - return (e.stack || '').match(this.Regexp).reject(function(path) { - return /(prototype|unittest|update_helper)\.js/.test(path); - }).join("\n"); + var match = (e.stack || '').match(this.Regexp); + if (match) { + return match.reject(function(path) { + return (/(prototype|unittest|update_helper)\.js/).test(path); + }).join("\n"); + } else { return ''; } } }, diff --git a/src/ajax/periodical_updater.js b/src/ajax/periodical_updater.js deleted file mode 100644 index d5fa4ef10..000000000 --- a/src/ajax/periodical_updater.js +++ /dev/null @@ -1,111 +0,0 @@ -/** section: Ajax - * class Ajax.PeriodicalUpdater - * - * Periodically performs an Ajax request and updates a container's contents - * based on the response text. - * - * `Ajax.PeriodicalUpdater` behaves like [[Ajax.Updater]], but performs the - * update at a prescribed interval, rather than only once. (Note that it is - * _not_ a subclass of `Ajax.Updater`; it's a wrapper around it.) - * - * This class addresses the common need of periodical update, as required by - * all sorts of "polling" mechanisms (e.g., an online chatroom or an online - * mail client). - * - * The basic idea is to run a regular [[Ajax.Updater]] at regular intervals, - * keeping track of the response text so it can (optionally) react to - * receiving the exact same response consecutively. - * - *
Additional options
- * - * `Ajax.PeriodicalUpdater` features all the common options and callbacks - * described in the [[Ajax section]] — _plus_ those added by `Ajax.Updater`. - * - * It also provides two new options: - * - * * `frequency` ([[Number]]; default is `2`): How long, in seconds, to wait - * between the end of one request and the beginning of the next. - * * `decay` ([[Number]]; default is `1`): The rate at which the `frequency` - * grows when the response received is _exactly_ the same as the previous. - * The default of `1` means `frequency` will never grow; override the - * default if a stale response implies it's worthwhile to poll less often. - * If `decay` is set to `2`, for instance, `frequency` will double - * (2 seconds, 4 seconds, 8 seconds...) each consecutive time the result - * is the same; when the result is different once again, `frequency` will - * revert to its original value. - * - *
Disabling and re-enabling a PeriodicalUpdater
- * - * You can hit the brakes on a running `PeriodicalUpdater` by calling - * [[Ajax.PeriodicalUpdater#stop]]. If you wish to re-enable it later, call - * [[Ajax.PeriodicalUpdater#start]]. - * -**/ - -Ajax.PeriodicalUpdater = Class.create(Ajax.Base, { - /** - * new Ajax.PeriodicalUpdater(container, url[, options]) - * - container (String | Element): The DOM element whose contents to update - * as a result of the Ajax request. Can be a DOM node or a string that - * identifies a node's ID. - * - url (String): The URL to fetch. When the _same-origin_ policy is in - * effect (as it is in most cases), `url` **must** be a relative URL or an - * absolute URL that starts with a slash (i.e., it must not begin with - * `http`). - * - options (Object): Configuration for the request. See the - * [[Ajax section]] for more information. - * - * Creates a new `Ajax.PeriodicalUpdater`. - **/ - initialize: function($super, container, url, options) { - $super(options); - this.onComplete = this.options.onComplete; - - this.frequency = (this.options.frequency || 2); - this.decay = (this.options.decay || 1); - - this.updater = { }; - this.container = container; - this.url = url; - - this.start(); - }, - - /** - * Ajax.PeriodicalUpdater#start() -> undefined - * - * Starts the periodical updater (if it had previously been stopped with - * [[Ajax.PeriodicalUpdater#stop]]). - **/ - start: function() { - this.options.onComplete = this.updateComplete.bind(this); - this.onTimerEvent(); - }, - - /** - * Ajax.PeriodicalUpdater#stop() -> undefined - * - * Stops the periodical updater. - * - * Also calls the `onComplete` callback, if one has been defined. - **/ - stop: function() { - this.updater.options.onComplete = undefined; - clearTimeout(this.timer); - (this.onComplete || Prototype.emptyFunction).apply(this, arguments); - }, - - updateComplete: function(response) { - if (this.options.decay) { - this.decay = (response.responseText == this.lastText ? - this.decay * this.options.decay : 1); - - this.lastText = response.responseText; - } - this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency); - }, - - onTimerEvent: function() { - this.updater = new Ajax.Updater(this.container, this.url, this.options); - } -}); diff --git a/src/constants.yml b/src/constants.yml index 9be5d270a..2532f981c 100644 --- a/src/constants.yml +++ b/src/constants.yml @@ -1 +1 @@ -PROTOTYPE_VERSION: 1.6.1 +PROTOTYPE_VERSION: 1.7.3 diff --git a/src/dom/dom.js b/src/dom/dom.js deleted file mode 100644 index 1301bc7f2..000000000 --- a/src/dom/dom.js +++ /dev/null @@ -1,2040 +0,0 @@ -/** section: DOM, related to: Element - * $(id) -> Element - * $(id...) -> [Element]... - * - id (String | Element): A DOM node or a string that references a node's - * ID. - * - * If provided with a string, returns the element in the document with - * matching ID; otherwise returns the passed element. - * - * Takes in an arbitrary number of arguments. Returns one `Element` if given - * one argument; otherwise returns an array of `Element`s. - * - * All elements returned by the function are "extended" with `Element` - * instance methods. -**/ - -function $(element) { - if (arguments.length > 1) { - for (var i = 0, elements = [], length = arguments.length; i < length; i++) - elements.push($(arguments[i])); - return elements; - } - if (Object.isString(element)) - element = document.getElementById(element); - return Element.extend(element); -} - -if (Prototype.BrowserFeatures.XPath) { - document._getElementsByXPath = function(expression, parentElement) { - var results = []; - var query = document.evaluate(expression, $(parentElement) || document, - null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); - for (var i = 0, length = query.snapshotLength; i < length; i++) - results.push(Element.extend(query.snapshotItem(i))); - return results; - }; -} - -/*--------------------------------------------------------------------------*/ - -if (!window.Node) var Node = { }; - -if (!Node.ELEMENT_NODE) { - // DOM level 2 ECMAScript Language Binding - Object.extend(Node, { - ELEMENT_NODE: 1, - ATTRIBUTE_NODE: 2, - TEXT_NODE: 3, - CDATA_SECTION_NODE: 4, - ENTITY_REFERENCE_NODE: 5, - ENTITY_NODE: 6, - PROCESSING_INSTRUCTION_NODE: 7, - COMMENT_NODE: 8, - DOCUMENT_NODE: 9, - DOCUMENT_TYPE_NODE: 10, - DOCUMENT_FRAGMENT_NODE: 11, - NOTATION_NODE: 12 - }); -} - -/** section: DOM - * class Element -**/ - -/** - * new Element(tagName[, attributes]) - * - tagName (String): The name of the HTML element to create. - * - attributes (Object): A list of attribute/value pairs to set on the - * element. - * - * Creates an HTML element with `tagName` as the tag name. -**/ -(function(global) { - - // setAttribute is broken in IE (particularly when setting name attribute) - // see: http://msdn.microsoft.com/en-us/library/ms536389.aspx - var SETATTRIBUTE_IGNORES_NAME = (function(){ - var elForm = document.createElement("form"); - var elInput = document.createElement("input"); - var root = document.documentElement; - elInput.setAttribute("name", "test"); - elForm.appendChild(elInput); - root.appendChild(elForm); - var isBuggy = elForm.elements - ? (typeof elForm.elements.test == "undefined") - : null; - root.removeChild(elForm); - elForm = elInput = null; - return isBuggy; - })(); - - var element = global.Element; - global.Element = function(tagName, attributes) { - attributes = attributes || { }; - tagName = tagName.toLowerCase(); - var cache = Element.cache; - if (SETATTRIBUTE_IGNORES_NAME && attributes.name) { - tagName = '<' + tagName + ' name="' + attributes.name + '">'; - delete attributes.name; - return Element.writeAttribute(document.createElement(tagName), attributes); - } - if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName)); - return Element.writeAttribute(cache[tagName].cloneNode(false), attributes); - }; - Object.extend(global.Element, element || { }); - if (element) global.Element.prototype = element.prototype; -})(this); - -Element.cache = { }; -Element.idCounter = 1; - -Element.Methods = { - /** - * Element.visible(@element) -> boolean - * - * Tells whether `element` is visible (i.e., whether its inline `display` - * CSS property is set to `none`. - **/ - visible: function(element) { - return $(element).style.display != 'none'; - }, - - /** - * Element.toggle(@element) -> Element - * - * Toggles the visibility of `element`. Returns `element`. - **/ - toggle: function(element) { - element = $(element); - Element[Element.visible(element) ? 'hide' : 'show'](element); - return element; - }, - - - /** - * Element.hide(@element) -> Element - * - * Sets `display: none` on `element`. Returns `element`. - **/ - hide: function(element) { - element = $(element); - element.style.display = 'none'; - return element; - }, - - /** - * Element.show(@element) -> Element - * - * Removes `display: none` on `element`. Returns `element`. - **/ - show: function(element) { - element = $(element); - element.style.display = ''; - return element; - }, - - /** - * Element.remove(@element) -> Element - * - * Completely removes `element` from the document and returns it. - **/ - remove: function(element) { - element = $(element); - element.parentNode.removeChild(element); - return element; - }, - - /** - * Element.update(@element[, newContent]) -> Element - * - * Replaces _the content_ of `element` with the `newContent` argument and - * returns `element`. - * - * `newContent` may be in any of these forms: - * - [[String]]: A string of HTML to be parsed and rendered - * - [[Element]]: An Element instance to insert - * - ...any object with a `toElement` method: The method is called and the resulting element used - * - ...any object with a `toHTML` method: The method is called and the resulting HTML string - * is parsed and rendered - * - * If `newContent` is omitted, the element's content is blanked out (i.e., - * replaced with an empty string). - * - * If `newContent` is a string and contains one or more inline `End of test

".stripScripts(); - * // => "

This is a test.End of test

" - * - *
Caveat User
- * - * Note that the processing `stripScripts` does is good enough for most purposes, - * but you cannot rely on it for security purposes. If you're processing end-user-supplied - * content, `stripScripts` is probably not sufficiently robust to prevent hack attacks. - **/ - function stripScripts() { - return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), ''); - } - - /** - * String#extractScripts() -> Array - * - * Extracts the content of any script blocks present in the string and - * returns them as an array of strings. - **/ - function extractScripts() { - var matchAll = new RegExp(Prototype.ScriptFragment, 'img'); - var matchOne = new RegExp(Prototype.ScriptFragment, 'im'); - return (this.match(matchAll) || []).map(function(scriptTag) { - return (scriptTag.match(matchOne) || ['', ''])[1]; - }); - } - - /** - * String#evalScripts() -> Array - * - * Evaluates the content of any inline `'); + * // -> Element (and prints "updated!" in an alert dialog). + * $('fruits').innerHTML; + * // -> '

Kiwi, banana and apple.

' + * + * Relying on the `toString()` method: + * + * $('fruits').update(123); + * // -> Element + * $('fruits').innerHTML; + * // -> '123' + * + * Finally, you can do some pretty funky stuff by defining your own + * `toString()` method on your custom objects: + * + * var Fruit = Class.create({ + * initialize: function(fruit){ + * this.fruit = fruit; + * }, + * toString: function(){ + * return 'I am a fruit and my name is "' + this.fruit + '".'; + * } + * }); + * var apple = new Fruit('apple'); + * + * $('fruits').update(apple); + * $('fruits').innerHTML; + * // -> 'I am a fruit and my name is "apple".' + **/ + function update(element, content) { + element = $(element); + + // Purge the element's existing contents of all storage keys and + // event listeners, since said content will be replaced no matter + // what. + var descendants = element.getElementsByTagName('*'), + i = descendants.length; + while (i--) purgeElement(descendants[i]); + + if (content && content.toElement) + content = content.toElement(); + + if (Object.isElement(content)) + return element.update().insert(content); + + + content = Object.toHTML(content); + var tagName = element.tagName.toUpperCase(); + + if (ANY_INNERHTML_BUGGY) { + if (tagName in INSERTION_TRANSLATIONS.tags) { + while (element.firstChild) + element.removeChild(element.firstChild); + + var nodes = getContentFromAnonymousElement(tagName, content.stripScripts()); + for (var i = 0, node; node = nodes[i]; i++) + element.appendChild(node); + + } else { + element.innerHTML = content.stripScripts(); + } + } else { + element.innerHTML = content.stripScripts(); + } + + content.evalScripts.bind(content).defer(); + return element; + } + + /** + * Element.replace(@element[, newContent]) -> Element + * + * Replaces `element` _itself_ with `newContent` and returns `element`. + * + * Keep in mind that this method returns the element that has just been + * removed — not the element that took its place. + * + * `newContent` can be either plain text, an HTML snippet or any JavaScript + * object which has a `toString()` method. + * + * If `newContent` contains any `'); + * // -> Element (ul#favorite) and prints "removed!" in an alert dialog. + * + * $('fruits').innerHTML; + * // -> '

Melon, oranges and grapes.

' + * + * With plain text: + * + * $('still-first').replace('Melon, oranges and grapes.'); + * // -> Element (p#still-first) + * + * $('fruits').innerHTML; + * // -> 'Melon, oranges and grapes.' + * + * Finally, relying on the `toString()` method: + * + * $('fruits').replace(123); + * // -> Element + * + * $('food').innerHTML; + * // -> '123' + * + * ##### Warning + * + * Using [[Element.replace]] as an instance method (e.g., + * `$('foo').replace('

Bar

')`) causes errors in Opera 9 when used on + * `input` elements. The `replace` property is reserved on `input` elements + * as part of [Web Forms 2](http://www.whatwg.org/specs/web-forms/current-work/). + * As a workaround, use the generic version instead + * (`Element.replace('foo', '

Bar

')`). + * + **/ + function replace(element, content) { + element = $(element); + + if (content && content.toElement) { + content = content.toElement(); + } else if (!Object.isElement(content)) { + content = Object.toHTML(content); + var range = element.ownerDocument.createRange(); + range.selectNode(element); + content.evalScripts.bind(content).defer(); + content = range.createContextualFragment(content.stripScripts()); + } + + element.parentNode.replaceChild(content, element); + return element; + } + + var INSERTION_TRANSLATIONS = { + before: function(element, node) { + element.parentNode.insertBefore(node, element); + }, + top: function(element, node) { + element.insertBefore(node, element.firstChild); + }, + bottom: function(element, node) { + element.appendChild(node); + }, + after: function(element, node) { + element.parentNode.insertBefore(node, element.nextSibling); + }, + + tags: { + TABLE: ['', '
', 1], + TBODY: ['', '
', 2], + TR: ['', '
', 3], + TD: ['
', '
', 4], + SELECT: ['', 1] + } + }; + + var tags = INSERTION_TRANSLATIONS.tags; + + Object.extend(tags, { + THEAD: tags.TBODY, + TFOOT: tags.TBODY, + TH: tags.TD + }); + + function replace_IE(element, content) { + element = $(element); + if (content && content.toElement) + content = content.toElement(); + if (Object.isElement(content)) { + element.parentNode.replaceChild(content, element); + return element; + } + + content = Object.toHTML(content); + var parent = element.parentNode, tagName = parent.tagName.toUpperCase(); + + if (tagName in INSERTION_TRANSLATIONS.tags) { + var nextSibling = Element.next(element); + var fragments = getContentFromAnonymousElement( + tagName, content.stripScripts()); + + parent.removeChild(element); + + var iterator; + if (nextSibling) + iterator = function(node) { parent.insertBefore(node, nextSibling) }; + else + iterator = function(node) { parent.appendChild(node); } + + fragments.each(iterator); + } else { + // We don't need to special-case this one. + element.outerHTML = content.stripScripts(); + } + + content.evalScripts.bind(content).defer(); + return element; + } + + if ('outerHTML' in document.documentElement) + replace = replace_IE; + + function isContent(content) { + if (Object.isUndefined(content) || content === null) return false; + + if (Object.isString(content) || Object.isNumber(content)) return true; + if (Object.isElement(content)) return true; + if (content.toElement || content.toHTML) return true; + + return false; + } + + // This private method does the bulk of the work for Element#insert. The + // actual insert method handles argument normalization and multiple + // content insertions. + function insertContentAt(element, content, position) { + position = position.toLowerCase(); + var method = INSERTION_TRANSLATIONS[position]; + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) { + method(element, content); + return element; + } + + content = Object.toHTML(content); + var tagName = ((position === 'before' || position === 'after') ? + element.parentNode : element).tagName.toUpperCase(); + + var childNodes = getContentFromAnonymousElement(tagName, content.stripScripts()); + + if (position === 'top' || position === 'after') childNodes.reverse(); + + for (var i = 0, node; node = childNodes[i]; i++) + method(element, node); + + content.evalScripts.bind(content).defer(); + } + + /** + * Element.insert(@element, content) -> Element + * - content (String | Element | Object): The content to insert. + * + * Inserts content `above`, `below`, at the `top`, and/or at the `bottom` of + * the given element, depending on the option(s) given. + * + * `insert` accepts content in any of these forms: + * - [[String]]: A string of HTML to be parsed and rendered + * - [[Element]]: An Element instance to insert + * - ...any object with a `toElement` method: The method is called and the resulting element used + * - ...any object with a `toHTML` method: The method is called and the resulting HTML string + * is parsed and rendered + * + * The `content` argument can be the content to insert, in which case the + * implied insertion point is `bottom`, or an object that specifies one or + * more insertion points (e.g., `{ bottom: "foo", top: "bar" }`). + * + * Accepted insertion points are: + * - `before` (as `element`'s previous sibling) + * - `after` (as `element's` next sibling) + * - `top` (as `element`'s first child) + * - `bottom` (as `element`'s last child) + * + * Note that if the inserted HTML contains any ` + **/ + present: function(element) { + return $(element).value != ''; + }, + + /** + * Form.Element.activate(@element) -> Element + * + * Gives focus to a form control and selects its contents if it is a text + * input. + * + * This method is just a shortcut for focusing and selecting; therefore, + * these are equivalent (aside from the fact that the former one will __not__ + * return the field) : + * + * Form.Element.focus('myelement').select() + * $('myelement').activate() + * + * Guess which call is the nicest? ;) + **/ + activate: function(element) { + element = $(element); + try { + element.focus(); + if (element.select && (element.tagName.toLowerCase() != 'input' || + !(/^(?:button|reset|submit)$/i.test(element.type)))) + element.select(); + } catch (e) { } + return element; + }, + + /** + * Form.Element.disable(@element) -> Element + * + * Disables a form control, effectively preventing its value from changing + * until it is enabled again. + * + * This method sets the native `disabled` property of an element to `true`. + * You can use this property to check the state of a control. + * + * ##### Notes + * + * Disabled form controls are never serialized. + * + * Never disable a form control as a security measure without having + * validation for it server-side. A user with minimal experience of + * JavaScript can enable these fields on your site easily using any browser. + * Instead, use disabling as a usability enhancement - with it you can + * indicate that a specific value should not be changed at the time being. + **/ + disable: function(element) { + element = $(element); + element.disabled = true; + return element; + }, + + /** + * Form.Element.enable(@element) -> Element + * + * Enables a previously disabled form control. + **/ + enable: function(element) { + element = $(element); + element.disabled = false; + return element; + } +}; + +/*--------------------------------------------------------------------------*/ + +var Field = Form.Element; + +/** section: DOM, related to: Form + * $F(element) -> String | Array + * + * Returns the value of a form control. This is a convenience alias of + * [[Form.Element.getValue]]. Refer to it for full details. +**/ +var $F = Form.Element.Methods.getValue; + +/*--------------------------------------------------------------------------*/ + +Form.Element.Serializers = (function() { + function input(element, value) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + return inputSelector(element, value); + default: + return valueSelector(element, value); + } + } + + function inputSelector(element, value) { + if (Object.isUndefined(value)) + return element.checked ? element.value : null; + else element.checked = !!value; + } + + function valueSelector(element, value) { + if (Object.isUndefined(value)) return element.value; + else element.value = value; + } + + function select(element, value) { + if (Object.isUndefined(value)) + return (element.type === 'select-one' ? selectOne : selectMany)(element); + + var opt, currentValue, single = !Object.isArray(value); + for (var i = 0, length = element.length; i < length; i++) { + opt = element.options[i]; + currentValue = this.optionValue(opt); + if (single) { + if (currentValue == value) { + opt.selected = true; + return; + } + } + else opt.selected = value.include(currentValue); + } + } + + function selectOne(element) { + var index = element.selectedIndex; + return index >= 0 ? optionValue(element.options[index]) : null; + } + + function selectMany(element) { + var values, length = element.length; + if (!length) return null; + + for (var i = 0, values = []; i < length; i++) { + var opt = element.options[i]; + if (opt.selected) values.push(optionValue(opt)); + } + return values; + } + + function optionValue(opt) { + return Element.hasAttribute(opt, 'value') ? opt.value : opt.text; + } + + return { + input: input, + inputSelector: inputSelector, + textarea: valueSelector, + select: select, + selectOne: selectOne, + selectMany: selectMany, + optionValue: optionValue, + button: valueSelector + }; +})(); + +/*--------------------------------------------------------------------------*/ + +/** section: DOM + * Abstract +**/ + +/** + * class Abstract.TimedObserver + * + * An abstract DOM element observer class, subclasses of which can be used to + * periodically check a value and trigger a callback when the value has changed. + * + * A `TimedObserver` object will try to check a value using the `getValue()` + * instance method which must be defined by the subclass. There are two + * out-of-the-box subclasses: + * [[Form.Observer]], which serializes a form and triggers when the result has + * changed; and [[Form.Element.Observer]], which triggers when the value of a + * given form field changes. + * + * + * Using `TimedObserver` implementations is straightforward; simply instantiate + * them with appropriate arguments. For example: + * + * new Form.Element.Observer( + * 'myelement', + * 0.2, // 200 milliseconds + * function(el, value){ + * alert('The form control has changed value to: ' + value) + * } + * ) + * + * Now that we have instantiated an object, it will check the value of the form + * control every 0.2 seconds and alert us of any change. While it is useless to + * alert the user of his own input (like in the example), we could be doing + * something useful like updating a certain part of the UI or informing the + * application on server of stuff happening (over Ajax). + * + * The callback function is always called with 2 arguments: the element given + * when the observer instance was made and the actual value that has changed + * and caused the callback to be triggered in the first place. + * + * ##### Creating Your Own TimedObserver Implementations + * + * It's easy to create your own `TimedObserver` implementations: Simply subclass + * `TimedObserver` and provide the `getValue()` method. For example, this is the + * complete source code for [[Form.Element.Observer]]: + * + * Form.Element.Observer = Class.create(Abstract.TimedObserver, { + * getValue: function() { + * return Form.Element.getValue(this.element); + * } + * }); +**/ +Abstract.TimedObserver = Class.create(PeriodicalExecuter, { + /** + * new Abstract.TimedObserver(element, frequency, callback) + * - element (String | Element): The DOM element to watch. Can be an element + * instance or an ID. + * - frequency (Number): The frequency, in seconds — e.g., 0.33 to + * check for changes every third of a second. + * - callback (Function): The callback to trigger when the value changes. + * + * Initializes an [[Abstract.TimedObserver]]; used by subclasses. + **/ + initialize: function($super, element, frequency, callback) { + $super(callback, frequency); + this.element = $(element); + this.lastValue = this.getValue(); + }, + + execute: function() { + var value = this.getValue(); + if (Object.isString(this.lastValue) && Object.isString(value) ? + this.lastValue != value : String(this.lastValue) != String(value)) { + this.callback(this.element, value); + this.lastValue = value; + } + } +}); + +/** + * class Form.Element.Observer < Abstract.TimedObserver + * + * An [[Abstract.TimedObserver]] subclass that watches for changes to a form + * field's value. This triggers the callback when the form field's value + * (according to [[Form.Element.getValue]]) changes. (Note that when the value + * actually changes can vary from browser to browser, particularly with + * `select` boxes.) + * + * Form.Element observer implements the `getValue()` method using + * [[Form.Element.getValue]] on the given element. See [[Abstract.TimedObserver]] + * for general documentation on timed observers. +**/ +Form.Element.Observer = Class.create(Abstract.TimedObserver, { + /** + * new Form.Element.Observer(element, frequency, callback) + * - element (String | Element): The form element to watch. Can be an element instance or an ID. + * - frequency (Number): The frequency, in seconds — e.g., 0.33 to check for changes every + * third of a second. + * - callback (Function): The callback to trigger when the value changes. + * + * Creates a [[Form.Element.Observer]]. + **/ + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +/** + * class Form.Observer < Abstract.TimedObserver + * + * An [[Abstract.TimedObserver]] subclass that watches for changes to a form. + * The callback is triggered when the form changes — e.g., when any + * of its fields' values changes, when fields are added/removed, etc.; anything + * that affects the serialized form of the form (see [[Form#serialize]]). + * + * ##### Example + * + * In this example an `observer` is used to change the appearance of the form + * if any of the values had been changed. It returns to its initial state when + * the data is submitted (saved). + * + * language: html + *
+ *
+ * Login Preferences + *

Current settings:

+ *

+ * + * + *

+ *

Login options

+ *

+ * + * + *

+ *

+ * + * + *

+ * + *
+ *
+ * + * +**/ +Form.Observer = Class.create(Abstract.TimedObserver, { + /** + * new Form.Observer(element, frequency, callback) + * - element (String | Element): The element of the form to watch. Can be an element + * instance or an ID. + * - frequency (Number): The frequency, in seconds -- e.g., 0.33 to check for changes every + * third of a second. + * - callback (Function): The callback to trigger when the form changes. + * + * Creates a [[Form.Observer]]. + **/ + getValue: function() { + return Form.serialize(this.element); + } +}); + +/*--------------------------------------------------------------------------*/ + +/** + * class Abstract.EventObserver +**/ +Abstract.EventObserver = Class.create({ + initialize: function(element, callback) { + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + if (this.element.tagName.toLowerCase() == 'form') + this.registerFormCallbacks(); + else + this.registerCallback(this.element); + }, + + onElementEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + }, + + registerFormCallbacks: function() { + Form.getElements(this.element).each(this.registerCallback, this); + }, + + registerCallback: function(element) { + if (element.type) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + Event.observe(element, 'click', this.onElementEvent.bind(this)); + break; + default: + Event.observe(element, 'change', this.onElementEvent.bind(this)); + break; + } + } + } +}); + +/** + * class Form.Element.EventObserver < Abstract.EventObserver +**/ +Form.Element.EventObserver = Class.create(Abstract.EventObserver, { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +/** + * class Form.EventObserver < Abstract.EventObserver +**/ +Form.EventObserver = Class.create(Abstract.EventObserver, { + getValue: function() { + return Form.serialize(this.element); + } +}); diff --git a/src/prototype/dom/layout.js b/src/prototype/dom/layout.js new file mode 100644 index 000000000..89a4057c8 --- /dev/null +++ b/src/prototype/dom/layout.js @@ -0,0 +1,1614 @@ +(function() { + + // Converts a CSS percentage value to a decimal. + // Ex: toDecimal("30%"); // -> 0.3 + function toDecimal(pctString) { + var match = pctString.match(/^(\d+)%?$/i); + if (!match) return null; + return (Number(match[1]) / 100); + } + + // A bare-bones version of Element.getStyle. Needed because getStyle is + // public-facing and too user-friendly for our tastes. We need raw, + // non-normalized values. + // + // Camel-cased property names only. + function getRawStyle(element, style) { + element = $(element); + + // Try inline styles first. + var value = element.style[style]; + if (!value || value === 'auto') { + // Reluctantly retrieve the computed style. + var css = document.defaultView.getComputedStyle(element, null); + value = css ? css[style] : null; + } + + if (style === 'opacity') return value ? parseFloat(value) : 1.0; + return value === 'auto' ? null : value; + } + + // Quickly figures out the content width of an element. Used instead of + // `element.measure('width')` in several places below; we don't want to + // call back into layout code recursively if we don't have to. + // + // But this means it doesn't handle edge cases. Use it when you know the + // element in question is visible and will give accurate measurements. + function getContentWidth(element, context) { + var boxWidth = element.offsetWidth; + + var bl = getPixelValue(element, 'borderLeftWidth', context) || 0; + var br = getPixelValue(element, 'borderRightWidth', context) || 0; + var pl = getPixelValue(element, 'paddingLeft', context) || 0; + var pr = getPixelValue(element, 'paddingRight', context) || 0; + + return boxWidth - bl - br - pl - pr; + } + + // Can be called like this: + // getPixelValue("11px"); + // Or like this: + // getPixelValue(someElement, 'paddingTop'); + function getPixelValue(value, property, context) { + var element = null; + if (Object.isElement(value)) { + element = value; + value = getRawStyle(element, property); + } + + if (value === null || Object.isUndefined(value)) { + return null; + } + + // Non-IE browsers will always return pixels if possible. + // (We use parseFloat instead of parseInt because Firefox can return + // non-integer pixel values.) + if ((/^(?:-)?\d+(\.\d+)?(px)?$/i).test(value)) { + return window.parseFloat(value); + } + + var isPercentage = value.include('%'); + + // For other browsers, we have to do a bit of work. + // (At this point, only percentages should be left; all other CSS units + // are converted to pixels by getComputedStyle.) + if (element && isPercentage) { + // The `context` argument comes into play for percentage units; it's + // the thing that the unit represents a percentage of. When an + // absolutely-positioned element has a width of 50%, we know that's + // 50% of its offset parent. If it's `position: fixed` instead, we know + // it's 50% of the viewport. And so on. + context = context || element.parentNode; + var decimal = toDecimal(value), whole = null; + + var isHorizontal = property.include('left') || property.include('right') || + property.include('width'); + + var isVertical = property.include('top') || property.include('bottom') || + property.include('height'); + + if (context === document.viewport) { + if (isHorizontal) { + whole = document.viewport.getWidth(); + } else if (isVertical) { + whole = document.viewport.getHeight(); + } + } else { + if (isHorizontal) { + whole = $(context).measure('width'); + } else if (isVertical) { + whole = $(context).measure('height'); + } + } + + return (whole === null) ? 0 : whole * decimal; + } + + // If we get this far, we should probably give up. + return 0; + } + + // Shortcut for figuring out if an element is `display: none` or not. + function isDisplayed(element) { + while (element && element.parentNode) { + var display = element.getStyle('display'); + if (display === 'none') { + return false; + } + element = element.parentNode; + } + return true; + } + + // Converts the layout hash property names back to the CSS equivalents. + // For now, only the border properties differ. + function cssNameFor(key) { + if (key.include('border')) key = key + '-width'; + return key.camelize(); + } + + /** + * class Element.Layout < Hash + * + * A set of key/value pairs representing measurements of various + * dimensions of an element. + * + *

Overview

+ * + * The `Element.Layout` class is a specialized way to measure elements. + * It helps mitigate: + * + * * The convoluted steps often needed to get common measurements for + * elements. + * * The tendency of browsers to report measurements in non-pixel units. + * * The quirks that lead some browsers to report inaccurate measurements. + * * The difficulty of measuring elements that are hidden. + * + *

Usage

+ * + * Instantiate an `Element.Layout` class by passing an element into the + * constructor: + * + * var layout = new Element.Layout(someElement); + * + * You can also use [[Element.getLayout]], if you prefer. + * + * Once you have a layout object, retrieve properties using [[Hash]]'s + * familiar `get` and `set` syntax. + * + * layout.get('width'); //-> 400 + * layout.get('top'); //-> 180 + * + * The following are the CSS-related properties that can be retrieved. + * Nearly all of them map directly to their property names in CSS. (The + * only exception is for borders — e.g., `border-left` instead of + * `border-left-width`.) + * + * * `height` + * * `width` + * * `top` + * * `left` + * * `right` + * * `bottom` + * * `border-left` + * * `border-right` + * * `border-top` + * * `border-bottom` + * * `padding-left` + * * `padding-right` + * * `padding-top` + * * `padding-bottom` + * * `margin-top` + * * `margin-bottom` + * * `margin-left` + * * `margin-right` + * + * In addition, these "composite" properties can be retrieved: + * + * * `padding-box-width` (width of the content area, from the beginning of + * the left padding to the end of the right padding) + * * `padding-box-height` (height of the content area, from the beginning + * of the top padding to the end of the bottom padding) + * * `border-box-width` (width of the content area, from the outer edge of + * the left border to the outer edge of the right border) + * * `border-box-height` (height of the content area, from the outer edge + * of the top border to the outer edge of the bottom border) + * * `margin-box-width` (width of the content area, from the beginning of + * the left margin to the end of the right margin) + * * `margin-box-height` (height of the content area, from the beginning + * of the top margin to the end of the bottom margin) + * + *

Caching

+ * + * Because these properties can be costly to retrieve, `Element.Layout` + * behaves differently from an ordinary [[Hash]]. + * + * First: by default, values are "lazy-loaded" — they aren't computed + * until they're retrieved. To measure all properties at once, pass + * a second argument into the constructor: + * + * var layout = new Element.Layout(someElement, true); + * + * Second: once a particular value is computed, it's cached. Asking for + * the same property again will return the original value without + * re-computation. This means that **an instance of `Element.Layout` + * becomes stale when the element's dimensions change**. When this + * happens, obtain a new instance. + * + *

Hidden elements

+ * + * Because it's a common case to want the dimensions of a hidden element + * (e.g., for animations), it's possible to measure elements that are + * hidden with `display: none`. + * + * However, **it's only possible to measure a hidden element if its parent + * is visible**. If its parent (or any other ancestor) is hidden, any + * width and height measurements will return `0`, as will measurements for + * `top|bottom|left|right`. + * + **/ + Element.Layout = Class.create(Hash, { + /** + * new Element.Layout(element[, preCompute = false]) + * - element (Element): The element to be measured. + * - preCompute (Boolean): Whether to compute all values at once. Default + * is `false`. + * + * Declare a new layout hash. + * + * The `preCompute` argument determines whether measurements will be + * lazy-loaded or not. If you plan to use many different measurements, + * it's often more performant to pre-compute, as it minimizes the + * amount of overhead needed to measure. If you need only one or two + * measurements, it's probably not worth it. + **/ + initialize: function($super, element, preCompute) { + $super(); + this.element = $(element); + + // nullify all properties keys + Element.Layout.PROPERTIES.each( function(property) { + this._set(property, null); + }, this); + + // The 'preCompute' boolean tells us whether we should fetch all values + // at once. If so, we should do setup/teardown only once. We set a flag + // so that we can ignore calls to `_begin` and `_end` elsewhere. + if (preCompute) { + this._preComputing = true; + this._begin(); + Element.Layout.PROPERTIES.each( this._compute, this ); + this._end(); + this._preComputing = false; + } + }, + + _set: function(property, value) { + return Hash.prototype.set.call(this, property, value); + }, + + // TODO: Investigate. + set: function(property, value) { + throw "Properties of Element.Layout are read-only."; + }, + + /** + * Element.Layout#get(property) -> Number + * - property (String): One of the properties defined in + * [[Element.Layout.PROPERTIES]]. + * + * Retrieve the measurement specified by `property`. Will throw an error + * if the property is invalid. + * + * ##### Caveats + * + * * `Element.Layout` can measure the dimensions of an element hidden with + * CSS (`display: none`), but _only_ if its parent element is visible. + **/ + get: function($super, property) { + // Try to fetch from the cache. + var value = $super(property); + return value === null ? this._compute(property) : value; + }, + + // `_begin` and `_end` are two functions that are called internally + // before and after any measurement is done. In certain conditions (e.g., + // when hidden), elements need a "preparation" phase that ensures + // accuracy of measurements. + _begin: function() { + if (this._isPrepared()) return; + + var element = this.element; + if (isDisplayed(element)) { + this._setPrepared(true); + return; + } + + // If we get this far, it means this element is hidden. To get usable + // measurements, we must remove `display: none`, but in a manner that + // isn't noticeable to the user. That means we also set + // `visibility: hidden` to make it invisible, and `position: absolute` + // so that it won't alter the document flow when displayed. + // + // Once we do this, the element is "prepared," and we can make our + // measurements. When we're done, the `_end` method cleans up our + // changes. + + // Remember the original values for some styles we're going to alter. + var originalStyles = { + position: element.style.position || '', + width: element.style.width || '', + visibility: element.style.visibility || '', + display: element.style.display || '' + }; + + // We store them so that the `_end` method can retrieve them later. + element.store('prototype_original_styles', originalStyles); + + var position = getRawStyle(element, 'position'), width = element.offsetWidth; + + if (width === 0 || width === null) { + // Opera/IE won't report the true width of the element through + // `getComputedStyle` if it's hidden. If we got a nonsensical value, + // we need to show the element and try again. + element.style.display = 'block'; + width = element.offsetWidth; + } + + // Preserve the context in case we get a percentage value. + var context = (position === 'fixed') ? document.viewport : + element.parentNode; + + var tempStyles = { + visibility: 'hidden', + display: 'block' + }; + + // If the element's `position: fixed`, it's already out of the document + // flow, so it's both unnecessary and inaccurate to set + // `position: absolute`. + if (position !== 'fixed') tempStyles.position = 'absolute'; + + element.setStyle(tempStyles); + + var positionedWidth = element.offsetWidth, newWidth; + if (width && (positionedWidth === width)) { + // If the element's width is the same both before and after + // we set absolute positioning, that means: + // (a) it was already absolutely-positioned; or + // (b) it has an explicitly-set width, instead of width: auto. + // Either way, it means the element is the width it needs to be + // in order to report an accurate height. + newWidth = getContentWidth(element, context); + } else if (position === 'absolute' || position === 'fixed') { + // Absolute- and fixed-position elements' dimensions don't depend + // upon those of their parents. + newWidth = getContentWidth(element, context); + } else { + // Otherwise, the element's width depends upon the width of its + // parent. + var parent = element.parentNode, pLayout = parent.getLayout(); + + newWidth = pLayout.get('width') - + this.get('margin-left') - + this.get('border-left') - + this.get('padding-left') - + this.get('padding-right') - + this.get('border-right') - + this.get('margin-right'); + } + + // Whatever the case, we've now figured out the correct `width` value + // for the element. + element.setStyle({ width: newWidth + 'px' }); + + // The element is now ready for measuring. + this._setPrepared(true); + }, + + _end: function() { + var element = this.element; + var originalStyles = element.retrieve('prototype_original_styles'); + element.store('prototype_original_styles', null); + element.setStyle(originalStyles); + this._setPrepared(false); + }, + + _compute: function(property) { + var COMPUTATIONS = Element.Layout.COMPUTATIONS; + if (!(property in COMPUTATIONS)) { + throw "Property not found."; + } + + return this._set(property, COMPUTATIONS[property].call(this, this.element)); + }, + + _isPrepared: function() { + return this.element.retrieve('prototype_element_layout_prepared', false); + }, + + _setPrepared: function(bool) { + return this.element.store('prototype_element_layout_prepared', bool); + }, + + /** + * Element.Layout#toObject([keys...]) -> Object + * - keys (String): A space-separated list of keys to include. + * + * Converts the layout hash to a plain object of key/value pairs, + * optionally including only the given keys. + * + * Keys can be passed into this method as individual arguments _or_ + * separated by spaces within a string. + * + * // Equivalent statements: + * someLayout.toObject('top', 'bottom', 'left', 'right'); + * someLayout.toObject('top bottom left right'); + **/ + toObject: function() { + var args = $A(arguments); + var keys = (args.length === 0) ? Element.Layout.PROPERTIES : + args.join(' ').split(' '); + var obj = {}; + keys.each( function(key) { + // Key needs to be a valid Element.Layout property. + if (!Element.Layout.PROPERTIES.include(key)) return; + var value = this.get(key); + if (value != null) obj[key] = value; + }, this); + return obj; + }, + + /** + * Element.Layout#toHash([keys...]) -> Hash + * - keys (String): A space-separated list of keys to include. + * + * Converts the layout hash to an ordinary hash of key/value pairs, + * optionally including only the given keys. + * + * Keys can be passed into this method as individual arguments _or_ + * separated by spaces within a string. + * + * // Equivalent statements: + * someLayout.toHash('top', 'bottom', 'left', 'right'); + * someLayout.toHash('top bottom left right'); + **/ + toHash: function() { + var obj = this.toObject.apply(this, arguments); + return new Hash(obj); + }, + + /** + * Element.Layout#toCSS([keys...]) -> Object + * - keys (String): A space-separated list of keys to include. + * + * Converts the layout hash to a plain object of CSS property/value + * pairs, optionally including only the given keys. + * + * Keys can be passed into this method as individual arguments _or_ + * separated by spaces within a string. + * + * // Equivalent statements: + * someLayout.toCSS('top', 'bottom', 'left', 'right'); + * someLayout.toCSS('top bottom left right'); + * + * Useful for passing layout properties to [[Element.setStyle]]. + **/ + toCSS: function() { + var args = $A(arguments); + var keys = (args.length === 0) ? Element.Layout.PROPERTIES : + args.join(' ').split(' '); + var css = {}; + + keys.each( function(key) { + // Key needs to be a valid Element.Layout property... + if (!Element.Layout.PROPERTIES.include(key)) return; + // ...but not a composite property. + if (Element.Layout.COMPOSITE_PROPERTIES.include(key)) return; + + var value = this.get(key); + if (value != null) css[cssNameFor(key)] = value + 'px'; + }, this); + return css; + }, + + inspect: function() { + return "#"; + } + }); + + Object.extend(Element.Layout, { + /** + * Element.Layout.PROPERTIES = Array + * + * A list of all measurable properties. + **/ + PROPERTIES: $w('height width top left right bottom border-left border-right border-top border-bottom padding-left padding-right padding-top padding-bottom margin-top margin-bottom margin-left margin-right padding-box-width padding-box-height border-box-width border-box-height margin-box-width margin-box-height'), + + /** + * Element.Layout.COMPOSITE_PROPERTIES = Array + * + * A list of all composite properties. Composite properties don't map + * directly to CSS properties — they're combinations of other + * properties. + **/ + COMPOSITE_PROPERTIES: $w('padding-box-width padding-box-height margin-box-width margin-box-height border-box-width border-box-height'), + + COMPUTATIONS: { + 'height': function(element) { + if (!this._preComputing) this._begin(); + + var bHeight = this.get('border-box-height'); + if (bHeight <= 0) { + if (!this._preComputing) this._end(); + return 0; + } + + var bTop = this.get('border-top'), + bBottom = this.get('border-bottom'); + + var pTop = this.get('padding-top'), + pBottom = this.get('padding-bottom'); + + if (!this._preComputing) this._end(); + + return bHeight - bTop - bBottom - pTop - pBottom; + }, + + 'width': function(element) { + if (!this._preComputing) this._begin(); + + var bWidth = this.get('border-box-width'); + if (bWidth <= 0) { + if (!this._preComputing) this._end(); + return 0; + } + + var bLeft = this.get('border-left'), + bRight = this.get('border-right'); + + var pLeft = this.get('padding-left'), + pRight = this.get('padding-right'); + + if (!this._preComputing) this._end(); + return bWidth - bLeft - bRight - pLeft - pRight; + }, + + 'padding-box-height': function(element) { + var height = this.get('height'), + pTop = this.get('padding-top'), + pBottom = this.get('padding-bottom'); + + return height + pTop + pBottom; + }, + + 'padding-box-width': function(element) { + var width = this.get('width'), + pLeft = this.get('padding-left'), + pRight = this.get('padding-right'); + + return width + pLeft + pRight; + }, + + 'border-box-height': function(element) { + if (!this._preComputing) this._begin(); + var height = element.offsetHeight; + if (!this._preComputing) this._end(); + return height; + }, + + 'border-box-width': function(element) { + if (!this._preComputing) this._begin(); + var width = element.offsetWidth; + if (!this._preComputing) this._end(); + return width; + }, + + 'margin-box-height': function(element) { + var bHeight = this.get('border-box-height'), + mTop = this.get('margin-top'), + mBottom = this.get('margin-bottom'); + + if (bHeight <= 0) return 0; + + return bHeight + mTop + mBottom; + }, + + 'margin-box-width': function(element) { + var bWidth = this.get('border-box-width'), + mLeft = this.get('margin-left'), + mRight = this.get('margin-right'); + + if (bWidth <= 0) return 0; + + return bWidth + mLeft + mRight; + }, + + 'top': function(element) { + var offset = element.positionedOffset(); + return offset.top; + }, + + 'bottom': function(element) { + var parent = element.getOffsetParent(); + var rect = element.getBoundingClientRect(), + pRect = parent.getBoundingClientRect(); + + return (pRect.bottom - rect.bottom).round(); + }, + + 'left': function(element) { + var offset = element.positionedOffset(); + return offset.left; + }, + + 'right': function(element) { + var parent = element.getOffsetParent(); + var rect = element.getBoundingClientRect(), + pRect = parent.getBoundingClientRect(); + + return (pRect.right - rect.right).round(); + }, + + 'padding-top': function(element) { + return getPixelValue(element, 'paddingTop'); + }, + + 'padding-bottom': function(element) { + return getPixelValue(element, 'paddingBottom'); + }, + + 'padding-left': function(element) { + return getPixelValue(element, 'paddingLeft'); + }, + + 'padding-right': function(element) { + return getPixelValue(element, 'paddingRight'); + }, + + 'border-top': function(element) { + return getPixelValue(element, 'borderTopWidth'); + }, + + 'border-bottom': function(element) { + return getPixelValue(element, 'borderBottomWidth'); + }, + + 'border-left': function(element) { + return getPixelValue(element, 'borderLeftWidth'); + }, + + 'border-right': function(element) { + return getPixelValue(element, 'borderRightWidth'); + }, + + 'margin-top': function(element) { + return getPixelValue(element, 'marginTop'); + }, + + 'margin-bottom': function(element) { + return getPixelValue(element, 'marginBottom'); + }, + + 'margin-left': function(element) { + return getPixelValue(element, 'marginLeft'); + }, + + 'margin-right': function(element) { + return getPixelValue(element, 'marginRight'); + } + } + }); + + /** + * class Element.Offset + * + * A representation of the top- and left-offsets of an element relative to + * another. + * + * All methods that compute offsets return an instance of `Element.Offset`. + * + **/ + Element.Offset = Class.create({ + /** + * new Element.Offset(left, top) + * + * Instantiates an [[Element.Offset]]. You shouldn't need to call this + * directly. + **/ + initialize: function(left, top) { + this.left = left.round(); + this.top = top.round(); + + // Act like an array. + this[0] = this.left; + this[1] = this.top; + }, + + /** + * Element.Offset#relativeTo(offset) -> Element.Offset + * - offset (Element.Offset): Another offset to compare to. + * + * Returns a new [[Element.Offset]] with its origin at the given + * `offset`. Useful for determining an element's distance from another + * arbitrary element. + **/ + relativeTo: function(offset) { + return new Element.Offset( + this.left - offset.left, + this.top - offset.top + ); + }, + + /** + * Element.Offset#inspect() -> String + * + * Returns a debug-friendly representation of the offset. + **/ + inspect: function() { + return "#".interpolate(this); + }, + + /** + * Element.Offset#toString() -> String + **/ + toString: function() { + return "[#{left}, #{top}]".interpolate(this); + }, + + /** + * Element.Offset#toArray() -> Array + * + * Returns an array representation fo the offset in [x, y] format. + **/ + toArray: function() { + return [this.left, this.top]; + } + }); + + /** + * Element.getLayout(@element[, preCompute = false]) -> Element.Layout + * - element (Element): The element to be measured. + * - preCompute (Boolean): Whether to compute all values at once. Default + * is `false`. + * + * Returns an instance of [[Element.Layout]] for measuring an element's + * dimensions. + * + * Note that this method returns a _new_ `Element.Layout` object each time + * it's called. If you want to take advantage of measurement caching, + * retain a reference to one `Element.Layout` object, rather than calling + * `Element.getLayout` whenever you need a measurement. You should call + * `Element.getLayout` again only when the values in an existing + * `Element.Layout` object have become outdated. + * + * If the `preCompute` argument is `true`, all properties will be measured + * when the layout object is instantiated. If you plan to measure several + * properties of an element's dimensions, it's probably worth it to get a + * pre-computed hash. + * + * ##### Examples + * + * var layout = $('troz').getLayout(); + * + * layout.get('width'); //-> 150 + * layout.get('height'); //-> 500 + * layout.get('padding-left'); //-> 10 + * layout.get('margin-left'); //-> 25 + * layout.get('border-top'); //-> 5 + * layout.get('border-bottom'); //-> 5 + * + * // Won't re-compute width; remembers value from first time. + * layout.get('width'); //-> 150 + * + * // Composite values obtained by adding together other properties; + * // will re-use any values we've already looked up above. + * layout.get('padding-box-width'); //-> 170 + * layout.get('border-box-height'); //-> 510 + * + * ##### Caveats + * + * * Instances of `Element.Layout` can measure the dimensions of an + * element hidden with CSS (`display: none`), but _only_ if its parent + * element is visible. + **/ + function getLayout(element, preCompute) { + return new Element.Layout(element, preCompute); + } + + /** + * Element.measure(@element, property) -> Number + * + * Gives the pixel value of `element`'s dimension specified by + * `property`. + * + * Useful for one-off measurements of elements. If you find yourself + * calling this method frequently over short spans of code, you might want + * to call [[Element.getLayout]] and operate on the [[Element.Layout]] + * object itself (thereby taking advantage of measurement caching). + * + * ##### Examples + * + * $('troz').measure('width'); //-> 150 + * $('troz').measure('border-top'); //-> 5 + * $('troz').measure('top'); //-> 226 + * + * ##### Caveats + * + * * `Element.measure` can measure the dimensions of an element hidden with + * CSS (`display: none`), but _only_ if its parent element is visible. + **/ + function measure(element, property) { + return $(element).getLayout().get(property); + } + + /** + * Element.getHeight(@element) -> Number + * + * Returns the height of `element`. + * + * This method returns correct values on elements whose display is set to + * `none` either in an inline style rule or in an CSS stylesheet. + * + * For performance reasons, if you need to query both width _and_ height of + * `element`, you should consider using [[Element.getDimensions]] instead. + * + * Note that the value returned is a _number only_ although it is + * _expressed in pixels_. + * + * ##### Examples + * + * language: html + *
+ * + * Then: + * + * $('rectangle').getHeight(); + * // -> 100 + **/ + function getHeight(element) { + return Element.getDimensions(element).height; + } + + /** + * Element.getWidth(@element) -> Number + * + * Returns the width of `element`. + * + * This method returns correct values on elements whose display is set to + * `none` either in an inline style rule or in an CSS stylesheet. + * + * For performance reasons, if you need to query both width _and_ height of + * `element`, you should consider using [[Element.getDimensions]] instead. + * + * Note that the value returned is a _number only_ although it is + * _expressed in pixels_. + * + * ##### Examples + * + * language: html + *
+ * + * Then: + * + * $('rectangle').getWidth(); + * // -> 200 + **/ + function getWidth(element) { + return Element.getDimensions(element).width; + } + + /** + * Element.getDimensions(@element) -> Object + * + * Finds the computed width and height of `element` and returns them as + * key/value pairs of an object. + * + * For backwards-compatibility, these dimensions represent the dimensions + * of the element's "border box" (including CSS padding and border). This + * is equivalent to the built-in `offsetWidth` and `offsetHeight` + * browser properties. + * + * Note that all values are returned as _numbers only_ although they are + * _expressed in pixels_. + * + * ##### Caveats + * + * * If the element is hidden via `display: none` in CSS, this method will + * attempt to measure the element by temporarily removing that CSS and + * applying `visibility: hidden` and `position: absolute`. This gives + * the element dimensions without making it visible or affecting the + * positioning of surrounding elements — but may not give accurate + * results in some cases. [[Element.measure]] is designed to give more + * accurate results. + * + * * In order to avoid calling the method twice, you should consider + * caching the returned values in a variable, as shown in the example + * below. + * + * * For more complex use cases, use [[Element.measure]], which is able + * to measure many different aspects of an element's dimensions and + * offsets. + * + * ##### Examples + * + * language: html + *
+ * + * Then: + * + * var dimensions = $('rectangle').getDimensions(); + * // -> {width: 200, height: 100} + * + * dimensions.width; + * // -> 200 + * + * dimensions.height; + * // -> 100 + **/ + function getDimensions(element) { + element = $(element); + var display = Element.getStyle(element, 'display'); + + if (display && display !== 'none') { + return { width: element.offsetWidth, height: element.offsetHeight }; + } + + // All *Width and *Height properties give 0 on elements with + // `display: none`, so show the element temporarily. + var style = element.style; + var originalStyles = { + visibility: style.visibility, + position: style.position, + display: style.display + }; + + var newStyles = { + visibility: 'hidden', + display: 'block' + }; + + // Switching `fixed` to `absolute` causes issues in Safari. + if (originalStyles.position !== 'fixed') + newStyles.position = 'absolute'; + + Element.setStyle(element, newStyles); + + var dimensions = { + width: element.offsetWidth, + height: element.offsetHeight + }; + + Element.setStyle(element, originalStyles); + + return dimensions; + } + + /** + * Element.getOffsetParent(@element) -> Element + * + * Returns `element`'s closest _positioned_ ancestor. If none is found, the + * `body` element is returned. + **/ + function getOffsetParent(element) { + element = $(element); + + // Ensure we never return the root HTML tag. + function selfOrBody(element) { + return isHtml(element) ? document.body : $(element); + } + + // For unusual cases like these, we standardize on returning the BODY + // element as the offset parent. + if (isDocument(element) || isDetached(element) || isBody(element) || isHtml(element)) + return document.body; + + // IE reports offset parent incorrectly for inline elements. + var isInline = (Element.getStyle(element, 'display') === 'inline'); + if (!isInline && element.offsetParent) return selfOrBody(element.offsetParent); + + while ((element = element.parentNode) && element !== document.body) { + if (Element.getStyle(element, 'position') !== 'static') { + return selfOrBody(element); + } + } + + return document.body; + } + + + /** + * Element.cumulativeOffset(@element) -> Element.Offset + * + * Returns the offsets of `element` from the top left corner of the + * document. + **/ + function cumulativeOffset(element) { + element = $(element); + var valueT = 0, valueL = 0; + if (element.parentNode) { + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + } + return new Element.Offset(valueL, valueT); + } + + /** + * Element.positionedOffset(@element) -> Element.Offset + * + * Returns `element`'s offset relative to its closest positioned ancestor + * (the element that would be returned by [[Element.getOffsetParent]]). + **/ + function positionedOffset(element) { + element = $(element); + + // Account for the margin of the element. + var layout = element.getLayout(); + + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + if (element) { + if (isBody(element)) break; + var p = Element.getStyle(element, 'position'); + if (p !== 'static') break; + } + } while (element); + + valueL -= layout.get('margin-left'); + valueT -= layout.get('margin-top'); + + return new Element.Offset(valueL, valueT); + } + + /** + * Element.cumulativeScrollOffset(@element) -> Element.Offset + * + * Calculates the cumulative scroll offset of an element in nested + * scrolling containers. + **/ + function cumulativeScrollOffset(element) { + var valueT = 0, valueL = 0; + do { + if (element === document.body) { + var bodyScrollNode = document.documentElement || document.body.parentNode || document.body; + valueT += !Object.isUndefined(window.pageYOffset) ? window.pageYOffset : bodyScrollNode.scrollTop || 0; + valueL += !Object.isUndefined(window.pageXOffset) ? window.pageXOffset : bodyScrollNode.scrollLeft || 0; + break; + } else { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } + } while (element); + return new Element.Offset(valueL, valueT); + } + + /** + * Element.viewportOffset(@element) -> Element.Offset + * + * Returns the X/Y coordinates of element relative to the viewport. + **/ + function viewportOffset(element) { + element = $(element); + if (isDetached(element)) return new Element.Offset(0, 0); + + var rect = element.getBoundingClientRect(); + return new Element.Offset(rect.left, rect.top); + } + + /** + * Element.absolutize(@element) -> Element + * + * Turns `element` into an absolutely-positioned element _without_ + * changing its position in the page layout. + **/ + function absolutize(element) { + element = $(element); + + if (Element.getStyle(element, 'position') === 'absolute') { + return element; + } + + var offsetParent = getOffsetParent(element); + var eOffset = element.viewportOffset(), + pOffset = offsetParent.viewportOffset(); + + var offset = eOffset.relativeTo(pOffset); + var layout = element.getLayout(); + + element.store('prototype_absolutize_original_styles', { + position: element.getStyle('position'), + left: element.getStyle('left'), + top: element.getStyle('top'), + width: element.getStyle('width'), + height: element.getStyle('height') + }); + + element.setStyle({ + position: 'absolute', + top: offset.top + 'px', + left: offset.left + 'px', + width: layout.get('width') + 'px', + height: layout.get('height') + 'px' + }); + + return element; + } + + /** + * Element.relativize(@element) -> Element + * + * Turns `element` into a relatively-positioned element without changing + * its position in the page layout. + * + * Used to undo a call to [[Element.absolutize]]. + **/ + function relativize(element) { + element = $(element); + if (Element.getStyle(element, 'position') === 'relative') { + return element; + } + + // Restore the original styles as captured by Element#absolutize. + var originalStyles = + element.retrieve('prototype_absolutize_original_styles'); + + if (originalStyles) element.setStyle(originalStyles); + return element; + } + + + /** + * Element.scrollTo(@element) -> Element + * + * Scrolls the window so that `element` appears at the top of the viewport. + * + * This has a similar effect than what would be achieved using + * [HTML anchors](http://www.w3.org/TR/html401/struct/links.html#h-12.2.3) + * (except the browser's history is not modified). + * + * ##### Example + * + * $(element).scrollTo(); + * // -> Element + **/ + function scrollTo(element) { + element = $(element); + var pos = Element.cumulativeOffset(element); + window.scrollTo(pos.left, pos.top); + return element; + } + + + /** + * Element.makePositioned(@element) -> Element + * + * Allows for the easy creation of a CSS containing block by setting + * `element`'s CSS `position` to `relative` if its initial position is + * either `static` or `undefined`. + * + * To revert back to `element`'s original CSS position, use + * [[Element.undoPositioned]]. + **/ + function makePositioned(element) { + element = $(element); + var position = Element.getStyle(element, 'position'), styles = {}; + if (position === 'static' || !position) { + styles.position = 'relative'; + Element.setStyle(element, styles); + Element.store(element, 'prototype_made_positioned', true); + } + return element; + } + + /** + * Element.undoPositioned(@element) -> Element + * + * Sets `element` back to the state it was in _before_ + * [[Element.makePositioned]] was applied to it. + * + * `element`'s absolutely positioned children will now have their positions + * set relatively to `element`'s nearest ancestor with a CSS `position` of + * `'absolute'`, `'relative'` or `'fixed'`. + **/ + function undoPositioned(element) { + element = $(element); + var storage = Element.getStorage(element), + madePositioned = storage.get('prototype_made_positioned'); + + if (madePositioned) { + storage.unset('prototype_made_positioned'); + Element.setStyle(element, { + position: '', + top: '', + bottom: '', + left: '', + right: '' + }); + } + return element; + } + + /** + * Element.makeClipping(@element) -> Element + * + * Simulates the poorly-supported CSS `clip` property by setting `element`'s + * `overflow` value to `hidden`. + * + * To undo clipping, use [[Element.undoClipping]]. + * + * The visible area is determined by `element`'s width and height. + * + * ##### Example + * + * language:html + *
+ * example + *
+ * + * Then: + * + * $('framer').makeClipping().setStyle({width: '100px', height: '100px'}); + * // -> Element + * + * Another example: + * + * language: html + * Click me to try it out. + * + *
+ * example + *
+ * + * + **/ + function makeClipping(element) { + element = $(element); + + var storage = Element.getStorage(element), + madeClipping = storage.get('prototype_made_clipping'); + + // The "prototype_made_clipping" storage key is meant to hold the + // original CSS overflow value. A string value or `null` means that we've + // called `makeClipping` already. An `undefined` value means we haven't. + if (Object.isUndefined(madeClipping)) { + var overflow = Element.getStyle(element, 'overflow'); + storage.set('prototype_made_clipping', overflow); + if (overflow !== 'hidden') + element.style.overflow = 'hidden'; + } + + return element; + } + + /** + * Element.undoClipping(@element) -> Element + * + * Sets `element`'s CSS `overflow` property back to the value it had + * _before_ [[Element.makeClipping]] was applied. + * + * ##### Example + * + * language: html + *
+ * example + *
+ * + * Then: + * + * $('framer').undoClipping(); + * // -> Element (and sets the CSS overflow property to its original value). + * + * Another example: + * + * language: html + * Click me to try it out. + * + *
+ * example + *
+ * + * + **/ + function undoClipping(element) { + element = $(element); + var storage = Element.getStorage(element), + overflow = storage.get('prototype_made_clipping'); + + if (!Object.isUndefined(overflow)) { + storage.unset('prototype_made_clipping'); + element.style.overflow = overflow || ''; + } + + return element; + } + + /** + * Element.clonePosition(@element, source[, options]) -> Element + * - source (Element | String): The source element (or its ID). + * - options (Object): The position fields to clone. + * + * Clones the position and/or dimensions of `source` onto the element as + * defined by `options`, with an optional offset for the `left` and `top` + * properties. + * + * Note that the element will be positioned exactly like `source` whether or + * not it is part of the same [CSS containing + * block](http://www.w3.org/TR/CSS21/visudet.html#containing-block-details). + * + * Also note that `element` must already be `position: absolute` or + * `position: fixed`. This method will not apply a `position` style. + * + * ##### Options + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
NameDefaultDescription
setLefttrueClones source's left CSS property onto element.
setToptrueClones source's top CSS property onto element.
setWidthtrueClones source's width onto element.
setHeighttrueClones source's width onto element.
offsetLeft0Number by which to offset element's left CSS property.
offsetTop0Number by which to offset element's top CSS property.
+ **/ + function clonePosition(element, source, options) { + options = Object.extend({ + setLeft: true, + setTop: true, + setWidth: true, + setHeight: true, + offsetTop: 0, + offsetLeft: 0 + }, options || {}); + + // Find page position of source. + source = $(source); + element = $(element); + var p, delta, layout, styles = {}; + + var isAbsolute = Element.getStyle(element, 'position') === 'absolute'; + var parent = Element.getOffsetParent(element); + + if (options.setLeft || options.setTop) { + // We start by measuring the source's viewport offset. + p = Element.viewportOffset(source); + + // If the element we're altering is `position: fixed`, that's all the + // information we need: later we'll apply that offset to the `top` and + // `left` properties directly. + delta = [0, 0]; + + // But if it's `position: absolute`, we have to know where its offset + // parent is positioned and take those measurements into account as + // well. + if (isAbsolute && parent !== document.body) { + delta = Element.viewportOffset(parent); + } + } + + function pageScrollXY() { + var x = 0, y = 0; + if (Object.isNumber(window.pageXOffset)) { + // Modern browsers. + x = window.pageXOffset; + y = window.pageYOffset; + } else if (document.body && (document.body.scrollLeft || document.body.scrollTop)) { + x = document.body.scrollLeft; + y = document.body.scrollTop; + } + return { x: x, y: y }; + } + + // When the offset parent is the document body, we need to account for + // scroll offsets when we set `top` and `left`. (Unless the element is + // `position: fixed`; in that case we should always ignore scroll + // position.) + var pageXY = (isAbsolute && parent === document.body) ? pageScrollXY() : { x: 0, y: 0 }; + + // Set position. + if (options.setLeft) + styles.left = (p[0] + pageXY.x - delta[0] + options.offsetLeft) + 'px'; + if (options.setTop) + styles.top = (p[1] + pageXY.y - delta[1] + options.offsetTop) + 'px'; + + if (options.setWidth || options.setHeight) { + layout = Element.getLayout(source); + + // Use content box when setting width/height. If padding/border are + // different between source and target, that's for the user to fix; + // there's no good option for us. + if (options.setWidth) { + styles.width = layout.get('width') + 'px'; + } + if (options.setHeight) { + styles.height = layout.get('height') + 'px'; + } + } + + return Element.setStyle(element, styles); + } + + + Element.addMethods({ + getLayout: getLayout, + measure: measure, + getWidth: getWidth, + getHeight: getHeight, + getDimensions: getDimensions, + getOffsetParent: getOffsetParent, + cumulativeOffset: cumulativeOffset, + positionedOffset: positionedOffset, + cumulativeScrollOffset: cumulativeScrollOffset, + viewportOffset: viewportOffset, + absolutize: absolutize, + relativize: relativize, + scrollTo: scrollTo, + makePositioned: makePositioned, + undoPositioned: undoPositioned, + makeClipping: makeClipping, + undoClipping: undoClipping, + clonePosition: clonePosition + }); + + function isBody(element) { + return element.nodeName.toUpperCase() === 'BODY'; + } + + function isHtml(element) { + return element.nodeName.toUpperCase() === 'HTML'; + } + + function isDocument(element) { + return element.nodeType === Node.DOCUMENT_NODE; + } + + function isDetached(element) { + return element !== document.body && + !Element.descendantOf(element, document.body); + } + +})(); + +(function() { + /** + * document.viewport + * + * The `document.viewport` namespace contains methods that return information + * about the viewport — the rectangle that represents the portion of a web + * page within view. In other words, it's the browser window minus all chrome. + **/ + + var ROOT = null; + function getRootElement() { + if (ROOT) return ROOT; + ROOT = document.documentElement; + return ROOT; + } + + /** + * document.viewport.getDimensions() -> Object + * + * Returns an object containing viewport dimensions in the form + * `{ width: Number, height: Number }`. + * + * The _viewport_ is the subset of the browser window that a page occupies + * — the "usable" space in a browser window. + * + * ##### Example + * + * document.viewport.getDimensions(); + * //-> { width: 776, height: 580 } + **/ + function getDimensions() { + return { width: this.getWidth(), height: this.getHeight() }; + } + + /** + * document.viewport.getWidth() -> Number + * + * Returns the width of the viewport. + * + * Equivalent to calling `document.viewport.getDimensions().width`. + **/ + function getWidth() { + return getRootElement().clientWidth; + } + + /** + * document.viewport.getHeight() -> Number + * + * Returns the height of the viewport. + * + * Equivalent to `document.viewport.getDimensions().height`. + **/ + function getHeight() { + return getRootElement().clientHeight; + } + + /** + * document.viewport.getScrollOffsets() -> Array + * + * Returns the viewport's horizontal and vertical scroll offsets. + * + * Returns an array in the form of `[leftValue, topValue]`. Also accessible + * as properties: `{ left: leftValue, top: topValue }`. + * + * ##### Examples + * + * document.viewport.getScrollOffsets(); + * //-> { left: 0, top: 0 } + * + * window.scrollTo(0, 120); + * document.viewport.getScrollOffsets(); + * //-> { left: 0, top: 120 } + **/ + function getScrollOffsets() { + var x = window.pageXOffset || document.documentElement.scrollLeft || + document.body.scrollLeft; + var y = window.pageYOffset || document.documentElement.scrollTop || + document.body.scrollTop; + + return new Element.Offset(x, y); + } + + document.viewport = { + getDimensions: getDimensions, + getWidth: getWidth, + getHeight: getHeight, + getScrollOffsets: getScrollOffsets + }; + +})(); diff --git a/src/prototype/dom/selector.js b/src/prototype/dom/selector.js new file mode 100644 index 000000000..356e4ef01 --- /dev/null +++ b/src/prototype/dom/selector.js @@ -0,0 +1,169 @@ +/** section: DOM, related to: Prototype.Selector + * $$(cssRule...) -> [Element...] + * + * Takes an arbitrary number of CSS selectors (strings) and returns a document-order + * array of extended DOM elements that match any of them. + * + * Sometimes the usual tools from your DOM arsenal -- `document.getElementById` encapsulated + * by [[$]], `getElementsByTagName` and even Prototype's very own `getElementsByClassName` + * extensions -- just aren't enough to quickly find elements or collections of elements. + * If you know the DOM tree structure, you can simply resort to CSS selectors to get + * the job done. + * + * ##### Quick examples + * + * $$('div'); + * // -> all DIVs in the document. Same as document.getElementsByTagName('div'). + * // Nice addition, the elements you're getting back are already extended! + * + * $$('#contents'); + * // -> same as $('contents'), only it returns an array anyway (even though IDs must + * // be unique within a document). + * + * $$('li.faux'); + * // -> all LI elements with class 'faux' + * + * The [[$$]] function searches the entire document. For selector queries on more specific + * sections of a document, use [[Element.select]]. + * + * ##### Supported CSS syntax + * + * The [[$$]] function does not rely on the browser's internal CSS parsing capabilities + * (otherwise, we'd be in cross-browser trouble...), and therefore offers a consistent + * set of selectors across all supported browsers. + * + * ###### Supported in v1.5.0 + * + * * Type selector: tag names, as in `div`. + * * Descendant selector: the space(s) between other selectors, as in `#a li`. + * * Attribute selectors: the full CSS 2.1 set of `[attr]`, `[attr=value]`, `[attr~=value]` + * and `[attr|=value]`. It also supports `[attr!=value]`. If the value you're matching + * against includes a space, be sure to enclose the value in quotation marks (`[title="Hello World!"]`). + * * Class selector: CSS class names, as in `.highlighted` or `.example.wrong`. + * * ID selector: as in `#item1`. + * + * ###### Supported from v1.5.1 + * + * Virtually all of [CSS3](http://www.w3.org/TR/2001/CR-css3-selectors-20011113/#selectors) + * is supported, with the exception of pseudo-elements (like `::first-letter`) and some + * pseudo-classes (like `:hover`). Some examples of new selectors that can be used in 1.5.1: + * + * * Child selector: selects immediate descendants, as in `#a > li`. + * * Attribute selectors: all attribute operators are supported, including `~=` (matches + * part of a space-delimited attribute value, like `rel` or `class`); `^=` (matches the + * beginning of a value); `$=` (matches the end of a value); and `*=` (matches any part + * of the value). + * * The `:not` pseudo-class, as in `#a *:not(li)` (matches all descendants of `#a` that + * aren't LIs). + * * All the `:nth`, `:first`, and `:last` pseudo-classes. Examples include `tr:nth-child(even)` + * (all even table rows), `li:first-child` (the first item in any list), or `p:nth-last-of-type(3)` + * (the third-to-last paragraph on the page). + * * The `:empty` pseudo-class (for selecting elements without children or text content). + * * The `:enabled`, `:disabled`, and `:checked` pseudo-classes (for use with form controls). + * + * ##### Examples + * + * $$('#contents a[rel]'); + * // -> all links inside the element of ID "contents" with a rel attribute + * + * $$('a[href="#"]'); + * // -> all links with a href attribute of value "#" (eyeew!) + * + * $$('#navbar a', '#sidebar a'); + * // -> all links within the elements of ID "navbar" or "sidebar" + * + * **With version 1.5.1 and above** you can do various types of advanced selectors: + * + * $$('a:not([rel~=nofollow])'); + * // -> all links, excluding those whose rel attribute contains the word "nofollow" + * + * $$('table tbody > tr:nth-child(even)'); + * // -> all even rows within all table bodies + * + * $$('div:empty'); + * // -> all DIVs without content (i.e., whitespace-only) +**/ +var $$ = function() { + var expression = $A(arguments).join(', '); + return Prototype.Selector.select(expression, document); +}; + +/** + * Prototype.Selector + * + * A namespace that acts as a wrapper around + * the choosen selector engine (Sizzle by default). + * +**/ +Prototype.Selector = (function() { + + var K = Prototype.K; + + /** + * Prototype.Selector.select(expression[, root = document]) -> [Element...] + * - expression (String): A CSS selector. + * - root (Element | document): A "scope" to search within. All results will + * be descendants of this node. + * + * Searches `root` for elements that match the provided CSS selector and returns an + * array of extended [[Element]] objects. + **/ + function select() { + throw new Error('Method "Prototype.Selector.select" must be defined.'); + } + + /** + * Prototype.Selector.match(element, expression) -> Boolean + * - element (Element): a DOM element. + * - expression (String): A CSS selector. + * + * Tests whether `element` matches the CSS selector. + **/ + function match() { + throw new Error('Method "Prototype.Selector.match" must be defined.'); + } + + /** + * Prototype.Selector.find(elements, expression[, index = 0]) -> Element + * - elements (Enumerable): a collection of DOM elements. + * - expression (String): A CSS selector. + * - index (Number): Numeric index of the match to return, defaults to 0. + * + * Filters the given collection of elements with `expression` and returns the + * first matching element (or the `index`th matching element if `index` is + * specified). + **/ + function find(elements, expression, index) { + index = index || 0; + var match = Prototype.Selector.match, length = elements.length, matchIndex = 0, i; + + for (i = 0; i < length; i++) { + if (match(elements[i], expression) && index == matchIndex++) { + return elements[i]; + } + } + } + + /** alias of: Element.extend + * Prototype.Selector.extendElement(element) -> Element + **/ + var extendElement = K; + + /** + * Prototype.Selector.extendElements(elements) -> Enumerable + * - elements (Enumerable): a collection of DOM elements. + * + * If necessary, extends the elements contained in `elements` + * and returns `elements` untouched. This is provided as a + * convenience method for selector engine wrapper implementors. + **/ + var extendElements = K; + + return { + select: select, + match: match, + find: find, + extendElements: K, + extendElement: K + }; +})(); diff --git a/src/prototype/lang.js b/src/prototype/lang.js new file mode 100644 index 000000000..b24801cee --- /dev/null +++ b/src/prototype/lang.js @@ -0,0 +1,81 @@ +//= compat +//= require "lang/class" +//= require "lang/object" +//= require "lang/function" +//= require "lang/regexp" +//= require "lang/periodical_executer" +//= require "lang/string" +//= require "lang/template" +//= require "lang/enumerable" +//= require "lang/array" +//= require "lang/hash" +//= require "lang/number" +//= require "lang/range" + +/** + * == Language == + * + * Additions to JavaScript's "standard library" and extensions to + * built-in JavaScript objects. +**/ + +var Abstract = { }; + +/** section: Language + * Try +**/ + +/** deprecated + * Try.these(function...) -> ? + * - function (Function): A function that may throw an exception. + * + * Accepts an arbitrary number of functions and returns the result of the + * first one that doesn't throw an error. + * + * **This method is deprecated.** + * + *
More information
+ * + * [[Try.these]] provides a simple idiom for trying out blocks of code in + * sequence. Such a sequence of attempts usually represents a downgrading + * approach to obtaining a given feature. + * + * In this example from Prototype's [[Ajax section]] internals, we want to get an + * `XMLHttpRequest` object. Internet Explorer 8 and earlier, however, does not + * provide it as a vanilla JavaScript object, and will throw an error if we + * attempt a simple instantiation. Also, over time, its proprietary way + * evolved, changing COM interface names. + * + * [[Try.these]] will try several ways in sequence, from the best (and, + * theoretically, most widespread) one to the oldest and rarest way, returning + * the result of the first successful function. + * + * If none of the blocks succeeded, [[Try.these]] will return `undefined`, which + * will cause the `Ajax.getTransport` method in the example below to return + * `false`, provided as a fallback result value. + * + * var Ajax = { + * getTransport: function() { + * return Try.these( + * function() { return new XMLHttpRequest() }, + * function() { return new ActiveXObject('Msxml2.XMLHTTP') }, + * function() { return new ActiveXObject('Microsoft.XMLHTTP') } + * ) || false; + * } + * }; + **/ +var Try = { + these: function() { + var returnValue; + + for (var i = 0, length = arguments.length; i < length; i++) { + var lambda = arguments[i]; + try { + returnValue = lambda(); + break; + } catch (e) { } + } + + return returnValue; + } +}; diff --git a/src/lang/array.js b/src/prototype/lang/array.js similarity index 60% rename from src/lang/array.js rename to src/prototype/lang/array.js index 389a1228d..f8d727d1b 100644 --- a/src/lang/array.js +++ b/src/prototype/lang/array.js @@ -1,29 +1,94 @@ /** section: Language, related to: Array - * $A(iterable) -> Array + * $A(iterable) -> Array * * Accepts an array-like collection (anything with numeric indices) and returns - * its equivalent as an actual Array object. - * This method is a convenience alias of [[Array.from]], but is the preferred way - * of casting to an Array. - **/ + * its equivalent as an actual [[Array]] object. This method is a convenience + * alias of [[Array.from]], but is the preferred way of casting to an [[Array]]. + * + * The primary use of [[$A]] is to obtain an actual [[Array]] object based on + * anything that could pass as an array (e.g. the `NodeList` or + * `HTMLCollection` objects returned by numerous DOM methods, or the predefined + * `arguments` reference within your functions). + * + * The reason you would want an actual [[Array]] is simple: + * [[Array Prototype extends Array]] to equip it with numerous extra methods, + * and also mixes in the [[Enumerable]] module, which brings in another + * boatload of nifty methods. Therefore, in Prototype, actual [[Array]]s trump + * any other collection type you might otherwise get. + * + * The conversion performed is rather simple: `null`, `undefined` and `false` become + * an empty array; any object featuring an explicit `toArray` method (as many Prototype + * objects do) has it invoked; otherwise, we assume the argument "looks like an array" + * (e.g. features a `length` property and the `[]` operator), and iterate over its components + * in the usual way. + * + * When passed an array, [[$A]] _makes a copy_ of that array and returns it. + * + * ##### Examples + * + * The well-known DOM method [`document.getElementsByTagName()`](http://www.w3.org/TR/DOM-Level-2-Core/core.html#ID-A6C9094) + * doesn't return an [[Array]], but a `NodeList` object that implements the basic array + * "interface.": + * + * var paras = $A(document.getElementsByTagName('p')); + * paras.each(Element.hide); + * $(paras.last()).show(); + * + * Notice we had to use [[Enumerable#each each]] and [[Element.hide]] because + * [[$A]] doesn't perform DOM extensions, since the array could contain + * anything (not just DOM elements). To use the [[Element#hide]] instance + * method we first must make sure all the target elements are extended: + * + * $A(document.getElementsByTagName('p')).map(Element.extend).invoke('hide'); + * + * Want to display your arguments easily? [[Array]] features a `join` method, but the `arguments` + * value that exists in all functions *does not* inherit from [[Array]]. So, the tough + * way, or the easy way? + * + * // The hard way... + * function showArgs() { + * alert(Array.prototype.join.call(arguments, ', ')); + * } + * + * // The easy way... + * function showArgs() { + * alert($A(arguments).join(', ')); + * } +**/ + function $A(iterable) { if (!iterable) return []; - // Safari <2.0.4 crashes when accessing property of a node list with property accessor. - // It nevertheless works fine with `in` operator, which is why we use it here - if ('toArray' in Object(iterable)) return iterable.toArray(); + if (iterable.toArray) return iterable.toArray(); var length = iterable.length || 0, results = new Array(length); while (length--) results[length] = iterable[length]; return results; } /** section: Language, related to: Array - * $w(string) -> Array - * - string (String): A string with zero or more spaces. + * $w(String) -> Array + * + * Splits a string into an [[Array]], treating all whitespace as delimiters. Equivalent + * to Ruby's `%w{foo bar}` or Perl's `qw(foo bar)`. + * + * This is one of those life-savers for people who just hate commas in literal arrays :-) + * + * ### Examples + * + * $w('apples bananas kiwis') + * // -> ['apples', 'bananas', 'kiwis'] + * + * This can slightly shorten code when writing simple iterations: * - * Splits a string into an array, treating all whitespace as delimiters. + * $w('apples bananas kiwis').each(function(fruit){ + * var message = 'I like ' + fruit + * // do something with the message + * }) * - * Equivalent to Ruby's `%w{foo bar}` or Perl's `qw(foo bar)`. + * This also becomes sweet when combined with [[Element]] functions: + * + * $w('ads navbar funkyLinks').each(Element.hide); **/ + function $w(string) { if (!Object.isString(string)) return []; string = string.strip(); @@ -52,7 +117,7 @@ Array.from = $A; * their `[]` indexing operator. They become very powerful objects that * greatly simplify the code for 99% of the common use cases involving them. * - *
Why you should stop using for...in to iterate
+ * ##### Why you should stop using for...in to iterate * * Many JavaScript authors have been misled into using the `for...in` JavaScript * construct to loop over array elements. This kind of code just won't work @@ -60,8 +125,8 @@ Array.from = $A; * * The ECMA 262 standard, which defines ECMAScript 3rd edition, supposedly * implemented by all major browsers including MSIE, defines ten methods - * on Array (§15.4.4), including nice methods like `concat`, `join`, `pop`, and - * `push`. + * on [[Array]] (§15.4.4), including nice methods like `concat`, `join`, + * `pop`, and `push`. * * This same standard explicitly defines that the `for...in` construct (§12.6.4) * exists to enumerate the properties of the object appearing on the right side @@ -75,9 +140,9 @@ Array.from = $A; * `Array.prototype` as non-enumerable. Therefore, using `for...in` on arrays * when using Prototype will enumerate all extended methods as well, such as * those coming from the [[Enumerable]] module, and those Prototype puts in the - * Array namespace (listed further below). + * [[Array]] namespace (listed further below). * - *
What you should use instead
+ * ##### What you should use instead * * You can revert to vanilla loops: * @@ -92,14 +157,12 @@ Array.from = $A; * // Your code working on item here... * }); * - * * The inability to use `for...in` on arrays is not much of a burden: as you'll * see, most of what you used to loop over arrays for can be concisely done * using the new methods provided by Array or the mixed-in [[Enumerable]] * module. So manual loops should be fairly rare. * - * - *
A note on performance
+ * ##### A note on performance * * Should you have a very large array, using iterators with lexical closures * (anonymous functions that you pass to the iterators and that get invoked at @@ -123,18 +186,25 @@ Array.from = $A; slice = arrayProto.slice, _each = arrayProto.forEach; // use native browser JS 1.6 implementation if available - function each(iterator) { - for (var i = 0, length = this.length; i < length; i++) - iterator(this[i]); - } - if (!_each) _each = each; + // Note that #map, #filter, #some, and #every take some extra steps for + // ES5 compliance: the context in which they're called is coerced to an + // object, and that object's `length` property is coerced to a finite + // integer. This makes it easier to use the methods as generics. + // + // This means that they behave a little differently from other methods in + // `Enumerable`/`Array` that don't collide with ES5, but that's OK. + + var filter = arrayProto.filter, + map = wrapNative(arrayProto.map), + some = wrapNative(arrayProto.some), + every = wrapNative(arrayProto.every); /** * Array#clear() -> Array * * Clears the array (makes it empty) and returns the array reference. * - *
Example
+ * ##### Example * * var guys = ['Sam', 'Justin', 'Andrew', 'Dan']; * guys.clear(); @@ -170,7 +240,7 @@ Array.from = $A; * * Returns a **copy** of the array without any `null` or `undefined` values. * - *
Example
+ * ##### Example * * var orig = [undefined, 'A', undefined, 'B', null, 'C']; * var copy = orig.compact(); @@ -193,7 +263,7 @@ Array.from = $A; * useful when handling the results of a recursive collection algorithm, * for instance. * - *
Example
+ * ##### Example * * var a = ['frank', ['bob', 'lisa'], ['jill', ['tom', 'sally']]]; * var b = a.flatten(); @@ -216,7 +286,7 @@ Array.from = $A; * Produces a new version of the array that does not contain any of the * specified values, leaving the original array unchanged. * - *
Examples
+ * ##### Examples * * [3, 5, 6].without(3) * // -> [5, 6] @@ -238,7 +308,7 @@ Array.from = $A; * * Reverses the array's contents, optionally cloning it first. * - *
Examples
+ * ##### Examples * * // Making a copy * var nums = [3, 5, 6, 1, 20]; @@ -258,7 +328,7 @@ Array.from = $A; /** * Array#uniq([sorted = false]) -> Array * - sorted (Boolean): Whether the array has already been sorted. If `true`, - * a less-costly algorithm will be used. + * a less-costly algorithm will be used. * * Produces a duplicate-free version of an array. If no duplicates are * found, the original array is returned. @@ -266,7 +336,7 @@ Array.from = $A; * On large arrays when `sorted` is `false`, this method has a potentially * large performance cost. * - *
Examples
+ * ##### Examples * * [1, 3, 2, 1].uniq(); * // -> [1, 2, 3] @@ -291,7 +361,7 @@ Array.from = $A; **/ function intersect(array) { return this.uniq().findAll(function(item) { - return array.detect(function(value) { return item === value }); + return array.indexOf(item) !== -1; }); } @@ -325,7 +395,7 @@ Array.from = $A; * * Returns the debug-oriented string representation of an array. * - *
Example
+ * ##### Example * * ['Apples', {good: 'yes', bad: 'no'}, 3, 34].inspect() * // -> "['Apples', [object Object], 3, 34]" @@ -334,92 +404,68 @@ Array.from = $A; return '[' + this.map(Object.inspect).join(', ') + ']'; } - /** related to: Object.toJSON - * Array#toJSON() -> String - * - * Returns a JSON string representation of the array. - * - *
Example
- * - * ['a', {b: null}].toJSON(); - * //-> '["a", {"b": null}]' - **/ - function toJSON() { - var results = []; - this.each(function(object) { - var value = Object.toJSON(object); - if (!Object.isUndefined(value)) results.push(value); - }); - return '[' + results.join(', ') + ']'; - } - - /** - * Array#indexOf(item[, offset = 0]) -> Number - * - item (?): A value that may or may not be in the array. - * - offset (Number): The number of initial items to skip before beginning - * the search. - * - * Returns the index of the first occurrence of `item` within the array, - * or `-1` if `item` doesn't exist in the array. `Array#indexOf` compares - * items using *strict equality* (`===`). - * - *
Examples
- * - * [3, 5, 6, 1, 20].indexOf(1) - * // -> 3 - * - * [3, 5, 6, 1, 20].indexOf(90) - * // -> -1 (not found) - * - * ['1', '2', '3'].indexOf(1); - * // -> -1 (not found, 1 !== '1') - **/ - function indexOf(item, i) { - i || (i = 0); - var length = this.length; - if (i < 0) i = length + i; - for (; i < length; i++) - if (this[i] === item) return i; - return -1; - } + // Certain ES5 array methods have the same names as Prototype array methods + // and perform the same functions. + // + // Prototype's implementations of these methods differ from the ES5 spec in + // the way a missing iterator function is handled. Prototype uses + // `Prototype.K` as a default iterator, while ES5 specifies that a + // `TypeError` must be thrown. Implementing the ES5 spec completely would + // break backward compatibility and would force users to pass `Prototype.K` + // manually. + // + // Instead, if native versions of these methods exist, we wrap the existing + // methods with our own behavior. This has very little performance impact. + // It violates the spec by suppressing `TypeError`s for certain methods, + // but that's an acceptable trade-off. - /** related to: Array#indexOf - * Array#lastIndexOf(item[, offset]) -> Number - * - item (?): A value that may or may not be in the array. - * - offset (Number): The number of items at the end to skip before beginning - * the search. - * - * Returns the position of the last occurrence of `item` within the array — or - * `-1` if `item` doesn't exist in the array. - **/ - function lastIndexOf(item, i) { - i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1; - var n = this.slice(0, i).reverse().indexOf(item); - return (n < 0) ? n : i - n - 1; - } - - // Replaces a built-in function. No PDoc needed. - function concat() { - var array = slice.call(this, 0), item; - for (var i = 0, length = arguments.length; i < length; i++) { - item = arguments[i]; - if (Object.isArray(item) && !('callee' in item)) { - for (var j = 0, arrayLength = item.length; j < arrayLength; j++) - array.push(item[j]); + function wrapNative(method) { + return function() { + if (arguments.length === 0) { + // No iterator was given. Instead of throwing a `TypeError`, use + // `Prototype.K` as the default iterator. + return method.call(this, Prototype.K); + } else if (arguments[0] === undefined) { + // Same as above. + var args = slice.call(arguments, 1); + args.unshift(Prototype.K); + return method.apply(this, args); } else { - array.push(item); + // Pass straight through to the native method. + return method.apply(this, arguments); } - } - return array; + }; } + // We used to define an `inject` method here that relied on ES5's + // `Array#reduce` (if present), but using `reduce` prevents us from + // catching a thrown `$break`. So arrays now use the standard + // `Enumerable.inject` like they did previously. + Object.extend(arrayProto, Enumerable); + // Enumerable's `entries` method is no longer safe to mixin to arrays, as + // it conflicts with an ES6 method. But it can still be mixed into other + // things. + if (arrayProto.entries === Enumerable.entries) { + delete arrayProto.entries; + } + if (!arrayProto._reverse) arrayProto._reverse = arrayProto.reverse; Object.extend(arrayProto, { _each: _each, + + map: map, + collect: map, + select: filter, + findAll: filter, + some: some, + any: some, + every: every, + all: every, + clear: clear, first: first, last: last, @@ -432,18 +478,6 @@ Array.from = $A; clone: clone, toArray: clone, size: size, - inspect: inspect, - toJSON: toJSON + inspect: inspect }); - - // fix for opera - var CONCAT_ARGUMENTS_BUGGY = (function() { - return [].concat(arguments)[0][0] !== 1; - })(1,2) - - if (CONCAT_ARGUMENTS_BUGGY) arrayProto.concat = concat; - - // use native browser JS 1.6 implementation if available - if (!arrayProto.indexOf) arrayProto.indexOf = indexOf; - if (!arrayProto.lastIndexOf) arrayProto.lastIndexOf = lastIndexOf; })(); diff --git a/src/lang/class.js b/src/prototype/lang/class.js similarity index 78% rename from src/lang/class.js rename to src/prototype/lang/class.js index 68b1fe649..e0167f74f 100644 --- a/src/lang/class.js +++ b/src/prototype/lang/class.js @@ -1,7 +1,7 @@ /* Based on Alex Arnell's inheritance implementation. */ /** section: Language - * Class + * class Class * * Manages Prototype's class-based OOP system. * @@ -9,6 +9,7 @@ * inheritance](http://prototypejs.org/learn/class-inheritance). **/ var Class = (function() { + /** * Class.create([superclass][, methods...]) -> Class * - superclass (Class): The optional superclass to inherit methods from. @@ -16,12 +17,12 @@ var Class = (function() { * new class. Any number of mixins can be added; later mixins take * precedence. * - * `Class.create` creates a class and returns a constructor function for + * [[Class.create]] creates a class and returns a constructor function for * instances of the class. Calling the constructor function (typically as * part of a `new` statement) will invoke the class's `initialize` method. * - * `Class.create` accepts two kinds of arguments. If the first argument is - * a `Class`, it's used as the new class's superclass, and all its methods + * [[Class.create]] accepts two kinds of arguments. If the first argument is + * a [[Class]], it's used as the new class's superclass, and all its methods * are inherited. Otherwise, any arguments passed are treated as objects, * and their methods are copied over ("mixed in") as instance methods of the * new class. In cases of method name overlap, later arguments take @@ -59,7 +60,7 @@ var Class = (function() { parent.subclasses.push(klass); } - for (var i = 0; i < properties.length; i++) + for (var i = 0, length = properties.length; i < length; i++) klass.addMethods(properties[i]); if (!klass.prototype.initialize) @@ -75,8 +76,8 @@ var Class = (function() { * * Adds methods to an existing class. * - * `Class#addMethods` is a method available on classes that have been - * defined with `Class.create`. It can be used to add new instance methods + * [[Class#addMethods]] is a method available on classes that have been + * defined with [[Class.create]]. It can be used to add new instance methods * to that class, or overwrite existing methods, after the class has been * defined. * @@ -86,7 +87,7 @@ var Class = (function() { * of the class and of all its subclasses, even those that have already been * instantiated. * - *
Examples
+ * ##### Examples * * var Animal = Class.create({ * initialize: function(name, sound) { @@ -135,29 +136,33 @@ var Class = (function() { * //-> alerts "You should probably run. He looks really mad." **/ function addMethods(source) { - var ancestor = this.superclass && this.superclass.prototype; - var properties = Object.keys(source); - - // IE6 doesn't enumerate toString and valueOf properties, - // Force copy if they're not coming from Object.prototype. - if (!Object.keys({ toString: true }).length) { - if (source.toString != Object.prototype.toString) - properties.push("toString"); - if (source.valueOf != Object.prototype.valueOf) - properties.push("valueOf"); - } + var ancestor = this.superclass && this.superclass.prototype, + properties = Object.keys(source); for (var i = 0, length = properties.length; i < length; i++) { var property = properties[i], value = source[property]; if (ancestor && Object.isFunction(value) && - value.argumentNames().first() == "$super") { + value.argumentNames()[0] == "$super") { var method = value; value = (function(m) { return function() { return ancestor[m].apply(this, arguments); }; })(property).wrap(method); - value.valueOf = method.valueOf.bind(method); - value.toString = method.toString.bind(method); + // We used to use `bind` to ensure that `toString` and `valueOf` + // methods were called in the proper context, but now that we're + // relying on native bind and/or an existing polyfill, we can't rely + // on the nuanced behavior of whatever `bind` implementation is on + // the page. + // + // MDC's polyfill, for instance, doesn't like binding functions that + // haven't got a `prototype` property defined. + value.valueOf = (function(method) { + return function() { return method.valueOf.call(method); }; + })(method); + + value.toString = (function(method) { + return function() { return method.toString.call(method); }; + })(method); } this.prototype[property] = value; } diff --git a/src/lang/enumerable.js b/src/prototype/lang/enumerable.js similarity index 87% rename from src/lang/enumerable.js rename to src/prototype/lang/enumerable.js index 598505be4..f55eac347 100644 --- a/src/lang/enumerable.js +++ b/src/prototype/lang/enumerable.js @@ -1,20 +1,20 @@ /** section: Language * mixin Enumerable * - * `Enumerable` provides a large set of useful methods for enumerations — + * [[Enumerable]] provides a large set of useful methods for enumerations — * objects that act as collections of values. It is a cornerstone of * Prototype. * - * `Enumerable` is a _mixin_: a set of methods intended not for standaone + * [[Enumerable]] is a _mixin_: a set of methods intended not for standalone * use, but for incorporation into other objects. * - * Prototype mixes `Enumerable` into several classes. The most visible cases + * Prototype mixes [[Enumerable]] into several classes. The most visible cases * are [[Array]] and [[Hash]], but you'll find it in less obvious spots as * well, such as in [[ObjectRange]] and various DOM- or Ajax-related objects. * - *
The context parameter
+ * ##### The `context` parameter * - * Every method of `Enumerable` that takes an iterator also takes the "context + * Every method of [[Enumerable]] that takes an iterator also takes the "context * object" as the next (optional) parameter. The context object is what the * iterator will be _bound_ to — what the keyword `this` will refer to inside * the iterator. @@ -29,21 +29,56 @@ * // -> { foo: 0, bar: 1, baz: 2} * * If there is no `context` argument, the iterator function will execute in - * the scope from which the `Enumerable` method itself was called. - * - *
Mixing Enumerable into your own objects
+ * the scope from which the [[Enumerable]] method itself was called. + * + * ##### Flow control + * + * You might find yourself missing the `break` and `continue` keywords that + * are available in ordinary `for` loops. If you need to break out of an + * enumeration before it's done, you can throw a special object named + * `$break`: + * + * var myObject = {}; + * + * ['foo', 'bar', 'baz', 'thud'].each( function(name, index) { + * if (name === 'baz') throw $break; + * myObject[name] = index; + * }); + * + * myObject; + * // -> { foo: 0, bar: 1 } + * + * Though we're technically throwing an exception, the `each` method knows + * to catch a thrown `$break` object and treat it as a command to stop + * iterating. (_Any_ exception thrown within an iterator will stop + * iteration, but only `$break` will be caught and suppressed.) + * + * If you need `continue`-like behavior, you can simply return early from + * your iterator: + * + * var myObject = {}; + * + * ['foo', 'bar', 'baz', 'thud'].each( function(name, index) { + * if (name === 'baz') return; + * myObject[name] = index; + * }); + * + * myObject; + * // -> { foo: 0, bar: 1, thud: 3 } + * + * ##### Mixing [[Enumerable]] into your own objects * * So, let's say you've created your very own collection-like object (say, * some sort of Set, or perhaps something that dynamically fetches data * ranges from the server side, lazy-loading style). You want to be able to - * mix `Enumerable` in (and we commend you for it). How do you go about this? + * mix [[Enumerable]] in (and we commend you for it). How do you go about this? * * The Enumerable module basically makes only one requirement on your object: * it must provide a method named `_each` (note the leading underscore) that * will accept a function as its unique argument, and will contain the actual * "raw iteration" algorithm, invoking its argument with each element in turn. * - * As detailed in the documentation for [[Enumerable#each]], `Enumerable` + * As detailed in the documentation for [[Enumerable#each]], [[Enumerable]] * provides all the extra layers (handling iteration short-circuits, passing * numeric indices, etc.). You just need to implement the actual iteration, * as fits your internal structure. @@ -52,9 +87,9 @@ * [[Array]], [[Hash]], or [[ObjectRange]]. They all begin with their own * `_each` method, which should help you grasp the idea. * - * Once you're done with this, you just need to mix `Enumerable` in, which + * Once you're done with this, you just need to mix [[Enumerable]] in, which * you'll usually do before defining your methods, so as to make sure whatever - * overrides you provide for `Enumerable` methods will indeed prevail. In + * overrides you provide for [[Enumerable]] methods will indeed prevail. In * short, your code will probably end up looking like this: * * @@ -93,25 +128,22 @@ var Enumerable = (function() { * * Calls `iterator` for each item in the collection. * - *
Examples
+ * ##### Examples * * ['one', 'two', 'three'].each(alert); * // Alerts "one", then alerts "two", then alerts "three" * - *
Built-In Variants
+ * ##### Built-In Variants * * Most of the common use cases for `each` are already available pre-coded - * as other methods on `Enumerable`. Whether you want to find the first + * as other methods on [[Enumerable]]. Whether you want to find the first * matching item in an enumeration, or transform it, or determine whether it - * has any (or all) values matching a particular condition, `Enumerable` + * has any (or all) values matching a particular condition, [[Enumerable]] * has a method to do that for you. **/ function each(iterator, context) { - var index = 0; try { - this._each(function(value) { - iterator.call(context, value, index++); - }); + this._each(iterator, context); } catch (e) { if (e != $break) throw e; } @@ -131,7 +163,7 @@ var Enumerable = (function() { * fewer than `number` items; it won't "pad" the last group with empty * values. For that behavior, use [[Enumerable#inGroupsOf]]. * - *
Example
+ * ##### Example * * var students = [ * { name: 'Sunny', age: 20 }, @@ -168,7 +200,7 @@ var Enumerable = (function() { * is boolean-equivalent to `false`, such as `undefined`, `0`, or indeed * `false`); * - *
Examples
+ * ##### Examples * * [].all(); * // -> true (empty arrays have no elements that could be falsy) @@ -186,9 +218,9 @@ var Enumerable = (function() { iterator = iterator || Prototype.K; var result = true; this.each(function(value, index) { - result = result && !!iterator.call(context, value, index); + result = result && !!iterator.call(context, value, index, this); if (!result) throw $break; - }); + }, this); return result; } @@ -203,7 +235,7 @@ var Enumerable = (function() { * Determines whether at least one element is truthy (boolean-equivalent to * `true`), either directly or through computation by the provided iterator. * - *
Examples
+ * ##### Examples * * [].any(); * // -> false (empty arrays have no elements that could be truthy) @@ -218,9 +250,9 @@ var Enumerable = (function() { iterator = iterator || Prototype.K; var result = false; this.each(function(value, index) { - if (result = !!iterator.call(context, value, index)) + if (result = !!iterator.call(context, value, index, this)) throw $break; - }); + }, this); return result; } @@ -235,7 +267,7 @@ var Enumerable = (function() { * `iterator` is provided, the elements are simply copied to the * returned array. * - *
Examples
+ * ##### Examples * * ['Hitch', "Hiker's", 'Guide', 'to', 'the', 'Galaxy'].collect(function(s) { * return s.charAt(0).toUpperCase(); @@ -251,8 +283,8 @@ var Enumerable = (function() { iterator = iterator || Prototype.K; var results = []; this.each(function(value, index) { - results.push(iterator.call(context, value, index)); - }); + results.push(iterator.call(context, value, index, this)); + }, this); return results; } @@ -266,7 +298,7 @@ var Enumerable = (function() { * Returns the first element for which the iterator returns a truthy value. * Aliased by the [[Enumerable#find]] method. * - *
Example
+ * ##### Example * * [1, 7, -2, -4, 5].detect(function(n) { return n < 0; }); * // -> -2 @@ -274,11 +306,11 @@ var Enumerable = (function() { function detect(iterator, context) { var result; this.each(function(value, index) { - if (iterator.call(context, value, index)) { + if (iterator.call(context, value, index, this)) { result = value; throw $break; } - }); + }, this); return result; } @@ -291,7 +323,7 @@ var Enumerable = (function() { * Returns all the elements for which the iterator returned a truthy value. * For the opposite operation, see [[Enumerable#reject]]. * - *
Example
+ * ##### Example * * [1, 'two', 3, 'four', 5].findAll(Object.isString); * // -> ['two', 'four'] @@ -299,9 +331,9 @@ var Enumerable = (function() { function findAll(iterator, context) { var results = []; this.each(function(value, index) { - if (iterator.call(context, value, index)) + if (iterator.call(context, value, index, this)) results.push(value); - }); + }, this); return results; } @@ -326,7 +358,7 @@ var Enumerable = (function() { * or a falsy value not to. Note that the `RegExp` `match` function will * convert elements to Strings to perform matching. * - *
Examples
+ * ##### Examples * * // Get all strings containing a repeated letter * ['hello', 'world', 'this', 'is', 'cool'].grep(/(.)\1/); @@ -345,8 +377,8 @@ var Enumerable = (function() { this.each(function(value, index) { if (filter.match(value)) - results.push(iterator.call(context, value, index)); - }); + results.push(iterator.call(context, value, index, this)); + }, this); return results; } @@ -358,7 +390,7 @@ var Enumerable = (function() { * based on the `==` comparison operator (equality with implicit type * conversion). * - *
Examples
+ * ##### Examples * * $R(1, 15).include(10); * // -> true @@ -370,8 +402,8 @@ var Enumerable = (function() { * // -> true ('3' == 3) **/ function include(object) { - if (Object.isFunction(this.indexOf)) - if (this.indexOf(object) != -1) return true; + if (Object.isFunction(this.indexOf) && this.indexOf(object) != -1) + return true; var found = false; this.each(function(value) { @@ -392,7 +424,7 @@ var Enumerable = (function() { * Like [[Enumerable#eachSlice]], but pads out the last chunk with the * specified value if necessary and doesn't support the `iterator` function. * - *
Examples
+ * ##### Examples * * var students = [ * { name: 'Sunny', age: 20 }, @@ -434,7 +466,7 @@ var Enumerable = (function() { * argument, the element as its second argument, and the element's index as * its third. It returns the new value for the accumulator. * - *
Examples
+ * ##### Examples * * $R(1,10).inject(0, function(acc, n) { return acc + n; }); * // -> 55 (sum of 1 to 10) @@ -449,8 +481,8 @@ var Enumerable = (function() { **/ function inject(memo, iterator, context) { this.each(function(value, index) { - memo = iterator.call(context, memo, value, index); - }); + memo = iterator.call(context, memo, value, index, this); + }, this); return memo; } @@ -462,7 +494,7 @@ var Enumerable = (function() { * Invokes the same method, with the same arguments, for all items in a * collection. Returns an array of the results of the method calls. * - *
Examples
+ * ##### Examples * * ['hello', 'world'].invoke('toUpperCase'); * // -> ['HELLO', 'WORLD'] @@ -499,7 +531,7 @@ var Enumerable = (function() { * evaluated, and its index in the enumeration; it should return the value * `max` should consider (and potentially return). * - *
Examples
+ * ##### Examples * * ['c', 'b', 'a'].max(); * // -> 'c' @@ -514,10 +546,10 @@ var Enumerable = (function() { iterator = iterator || Prototype.K; var result; this.each(function(value, index) { - value = iterator.call(context, value, index); + value = iterator.call(context, value, index, this); if (result == null || value >= result) result = value; - }); + }, this); return result; } @@ -539,7 +571,7 @@ var Enumerable = (function() { * evaluated, and its index in the enumeration; it should return the value * `min` should consider (and potentially return). * - *
Examples
+ * ##### Examples * * ['c', 'b', 'a'].min(); * // -> 'a' @@ -554,10 +586,10 @@ var Enumerable = (function() { iterator = iterator || Prototype.K; var result; this.each(function(value, index) { - value = iterator.call(context, value, index); + value = iterator.call(context, value, index, this); if (result == null || value < result) result = value; - }); + }, this); return result; } @@ -578,7 +610,7 @@ var Enumerable = (function() { * then using [[Enumerable#reject]] because the enumeration is only processed * once. * - *
Examples
+ * ##### Examples * * ['hello', null, 42, false, true, , 17].partition(); * // -> [['hello', 42, true, 17], [null, false, undefined]] @@ -592,9 +624,9 @@ var Enumerable = (function() { iterator = iterator || Prototype.K; var trues = [], falses = []; this.each(function(value, index) { - (iterator.call(context, value, index) ? + (iterator.call(context, value, index, this) ? trues : falses).push(value); - }); + }, this); return [trues, falses]; } @@ -606,7 +638,7 @@ var Enumerable = (function() { * and [[Enumerable#each]]: fetching the same property for all of the * elements. Returns an array of the property values. * - *
Example
+ * ##### Example * * ['hello', 'world', 'this', 'is', 'nice'].pluck('length'); * // -> [5, 5, 4, 2, 4] @@ -628,7 +660,7 @@ var Enumerable = (function() { * Returns all the elements for which the iterator returns a falsy value. * For the opposite operation, see [[Enumerable#findAll]]. * - *
Example
+ * ##### Example * * [1, "two", 3, "four", 5].reject(Object.isString); * // -> [1, 3, 5] @@ -636,9 +668,9 @@ var Enumerable = (function() { function reject(iterator, context) { var results = []; this.each(function(value, index) { - if (!iterator.call(context, value, index)) + if (!iterator.call(context, value, index, this)) results.push(value); - }); + }, this); return results; } @@ -654,10 +686,10 @@ var Enumerable = (function() { * well-defined ordering semantics (i.e. the `<` operator must exist between * any two criteria). * - * `sortBy` does not guarantee a *stable* sort; adjacent equivalent elements - * may be swapped. + * [[Enumerable#sortBy]] does not guarantee a *stable* sort; adjacent + * equivalent elements may be swapped. * - *
Example
+ * ##### Example * * ['hello', 'world', 'this', 'is', 'nice'].sortBy(function(s) { * return s.length; @@ -668,9 +700,9 @@ var Enumerable = (function() { return this.map(function(value, index) { return { value: value, - criteria: iterator.call(context, value, index) + criteria: iterator.call(context, value, index, this) }; - }).sort(function(left, right) { + }, this).sort(function(left, right) { var a = left.criteria, b = right.criteria; return a < b ? -1 : a > b ? 1 : 0; }).pluck('value'); @@ -681,7 +713,7 @@ var Enumerable = (function() { * * Returns an Array containing the elements of the enumeration. * - *
Example
+ * ##### Example * * $R(1, 5).toArray(); * // -> [1, 2, 3, 4, 5] @@ -708,7 +740,7 @@ var Enumerable = (function() { * If supplied, `iterator` is called with each tuple as its only argument * and should return the value to use in place of that tuple. * - *
Examples
+ * ##### Examples * * var firstNames = ['Jane', 'Nitin', 'Guy']; * var lastNames = ['Doe', 'Patel', 'Forcier']; diff --git a/src/lang/function.js b/src/prototype/lang/function.js similarity index 90% rename from src/lang/function.js rename to src/prototype/lang/function.js index 0d5611368..508e36fd6 100644 --- a/src/lang/function.js +++ b/src/prototype/lang/function.js @@ -24,7 +24,7 @@ Object.extend(Function.prototype, (function() { * the values as an array of strings (or an empty array if the function is * defined without parameters). * - *
Examples
+ * ##### Examples * * function fn(foo, bar) { * return foo + bar; @@ -52,9 +52,15 @@ Object.extend(Function.prototype, (function() { * function is called, it will call the original ensuring that `this` is set * to `context`. Also optionally curries arguments for the function. * - *
Examples
+ * `Function#bind` acts as an ECMAScript 5 [polyfill](http://remysharp.com/2010/10/08/what-is-a-polyfill/). + * It is only defined if not already present in the user's browser, and it + * is meant to behave like the native version as much as possible. Consult + * the [ES5 specification](http://es5.github.com/#x15.3.4.5) for more + * information. * - * A typical use of `Function#bind` is to ensure that a callback (event + * ##### Examples + * + * A typical use of [[Function#bind]] is to ensure that a callback (event * handler, etc.) that is an object method gets called with the correct * object as its context (`this` value): * @@ -105,13 +111,29 @@ Object.extend(Function.prototype, (function() { * * (To curry without binding, see [[Function#curry]].) **/ + function bind(context) { - if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this; + if (arguments.length < 2 && Object.isUndefined(arguments[0])) + return this; + + if (!Object.isFunction(this)) + throw new TypeError("The object is not callable."); + + var nop = function() {}; var __method = this, args = slice.call(arguments, 1); - return function() { + + var bound = function() { var a = merge(args, arguments); - return __method.apply(context, a); - } + // Ignore the supplied context when the bound function is called with + // the "new" keyword. + var c = this instanceof bound ? this : context; + return __method.apply(c, a); + }; + + nop.prototype = this.prototype; + bound.prototype = new nop(); + + return bound; } /** related to: Function#bind @@ -136,7 +158,7 @@ Object.extend(Function.prototype, (function() { * This mostly happens if the number of arguments will vary, and so you * want to know the event object is the first argument. * - *
Example
+ * ##### Example * * var ContentUpdater = Class.create({ * initialize: function(initialData) { @@ -195,7 +217,7 @@ Object.extend(Function.prototype, (function() { * f('a', 'b'); * // -> alerts "1, 2, 3, a, b" * - * `Function#curry` works just like [[Function#bind]] without the initial + * [[Function#curry]] works just like [[Function#bind]] without the initial * context argument. Use `bind` if you need to curry arguments _and_ set * context at the same time. * @@ -226,7 +248,7 @@ Object.extend(Function.prototype, (function() { * To schedule a function to run as soon as the interpreter is idle, use * [[Function#defer]]. * - *
Example
+ * ##### Example * * function showMsg(msg) { * alert(msg); @@ -255,7 +277,7 @@ Object.extend(Function.prototype, (function() { * ID that can be used to clear the timeout with `window.clearTimeout` before * it runs. * - *
Example
+ * ##### Example * * function showMsg(msg) { * alert(msg); @@ -278,12 +300,12 @@ Object.extend(Function.prototype, (function() { * * Returns a function "wrapped" around the original function. * - * `Function#wrap` distills the essence of aspect-oriented programming into + * [[Function#wrap]] distills the essence of aspect-oriented programming into * a single method, letting you easily build on existing functions by * specifying before and after behavior, transforming the return value, or * even preventing the original function from being called. * - * The wraper function is called with this signature: + * The wrapper function is called with this signature: * * function wrapper(callOriginal[, args...]) * @@ -293,7 +315,7 @@ Object.extend(Function.prototype, (function() { * indirection in-between that sets up the proper context \[`this` value\] for * it.) * - *
Example
+ * ##### Example * * // Wrap String#capitalize so it accepts an additional argument * String.prototype.capitalize = String.prototype.capitalize.wrap( @@ -333,7 +355,7 @@ Object.extend(Function.prototype, (function() { * want to transform a function that takes an object to a method of that * object or its prototype, shortening its signature by one argument. * - *
Example
+ * ##### Example * * // A function that sets a name on a target object * function setName(target, name) { @@ -376,16 +398,20 @@ Object.extend(Function.prototype, (function() { return __method.apply(null, a); }; } - - return { + + var extensions = { argumentNames: argumentNames, - bind: bind, bindAsEventListener: bindAsEventListener, curry: curry, delay: delay, defer: defer, wrap: wrap, methodize: methodize - } + }; + + if (!Function.prototype.bind) + extensions.bind = bind; + + return extensions; })()); diff --git a/src/lang/hash.js b/src/prototype/lang/hash.js similarity index 77% rename from src/lang/hash.js rename to src/prototype/lang/hash.js index 3ff86c04c..662640fcf 100644 --- a/src/lang/hash.js +++ b/src/prototype/lang/hash.js @@ -1,10 +1,23 @@ /** section: Language, related to: Hash - * $H([object]) -> Hash - * - * Creates a `Hash`. This is purely a convenience wrapper around the Hash - * constructor, it does not do anything other than pass any argument it's - * given into the Hash constructor and return the result. - **/ + * $H([obj]) -> Hash + * + * Creates a [[Hash]] (which is synonymous to "map" or "associative array" + * for our purposes). A convenience wrapper around the [[Hash]] constructor, with a safeguard + * that lets you pass an existing [[Hash]] object and get it back untouched (instead of + * uselessly cloning it). + * + * The [[$H]] function is the shorter way to obtain a hash (prior to 1.5 final, it was + * the *only* proper way of getting one). + * + * ##### Example + * + * var h = $H({name: 'John', age: 26, country: 'Australia'}); + * // Equivalent to: + * var h = new Hash({name: 'John', age: 26, country: 'Australia'}); + * // Can then be accessed the classic Hash way + * h.get('country'); + * // -> 'Australia' +**/ function $H(object) { return new Hash(object); }; @@ -15,18 +28,18 @@ function $H(object) { * * A set of key/value pairs. * - * `Hash` can be thought of as an associative array, binding unique keys to + * [[Hash]] can be thought of as an associative array, binding unique keys to * values (which are not necessarily unique), though it can not guarantee * consistent order its elements when iterating. Because of the nature of - * JavaScript, every object is in fact a hash; but `Hash` adds a number of + * JavaScript, every object is in fact a hash; but [[Hash]] adds a number of * methods that let you enumerate keys and values, iterate over key/value * pairs, merge two hashes together, and much more. * - *
Creating a hash
+ * ##### Creating a hash * * You can create a Hash either via `new Hash()` or the convenience alias * `$H()`; there is **no** difference between them. In either case, you may - * optionally pass in an object to seed the `Hash`. If you pass in a `Hash`, + * optionally pass in an object to seed the [[Hash]]. If you pass in a [[Hash]], * it will be cloned. * **/ @@ -34,7 +47,7 @@ var Hash = Class.create(Enumerable, (function() { /** * new Hash([object]) * - * Creates a new `Hash`. If `object` is given, the new hash will be populated + * Creates a new [[Hash]]. If `object` is given, the new hash will be populated * with all the object's properties. See [[$H]]. **/ function initialize(object) { @@ -44,7 +57,7 @@ var Hash = Class.create(Enumerable, (function() { // Docs for #each even though technically it's implemented by Enumerable /** * Hash#each(iterator[, context]) -> Hash - * - iterator (Function): A function that expects each item in the `Hash` + * - iterator (Function): A function that expects each item in the [[Hash]] * as the first argument and a numerical index as the second. * - context (Object): The scope in which to call `iterator`. Determines what * `this` means inside `iterator`. @@ -67,7 +80,7 @@ var Hash = Class.create(Enumerable, (function() { * implementations exhibit *ordered* behavior, this is not standardized and * may not always be the case, and so cannot be relied upon. * - *
Example
+ * ##### Example * * var h = $H({version: 1.6, author: 'The Core Team'}); * @@ -80,12 +93,14 @@ var Hash = Class.create(Enumerable, (function() { **/ // Our _internal_ each - function _each(iterator) { + function _each(iterator, context) { + var i = 0; for (var key in this._object) { var value = this._object[key], pair = [key, value]; pair.key = key; pair.value = value; - iterator(pair); + iterator.call(context, pair, i); + i++; } } @@ -96,7 +111,7 @@ var Hash = Class.create(Enumerable, (function() { * * Stores `value` in the hash using the key `key` and returns `value`. * - *
Example
+ * ##### Example * * var h = $H(); * h.keys(); @@ -117,7 +132,7 @@ var Hash = Class.create(Enumerable, (function() { * * Returns the stored value for the given `key`. * - *
Examples
+ * ##### Examples * * var h = new Hash({a: 'apple', b: 'banana', c: 'coconut'}); * h.get('a'); @@ -135,7 +150,7 @@ var Hash = Class.create(Enumerable, (function() { * Deletes the stored pair for the given `key` from the hash and returns its * value. * - *
Example
+ * ##### Example * * var h = new Hash({a: 'apple', b: 'banana', c: 'coconut'}); * h.keys(); @@ -157,7 +172,7 @@ var Hash = Class.create(Enumerable, (function() { * Returns a cloned, vanilla object whose properties (and property values) * match the keys (and values) from the hash. * - *
Example
+ * ##### Example * * var h = new Hash({ a: 'apple', b: 'banana', c: 'coconut' }); * var obj = h.toObject(); @@ -168,6 +183,14 @@ var Hash = Class.create(Enumerable, (function() { return Object.clone(this._object); } + /** related to: Object.toJSON, alias of: Hash#toObject + * Hash#toJSON() -> Object + **/ + + /** alias of: Hash#toObject + * Hash#toTemplateReplacements() -> Object + **/ + /** * Hash#keys() -> [String...] * @@ -175,7 +198,7 @@ var Hash = Class.create(Enumerable, (function() { * * The order of the keys is not guaranteed. * - *
Example
+ * ##### Example * * var h = $H({one: "uno", two: "due", three: "tre"}); * h.keys(); @@ -192,7 +215,7 @@ var Hash = Class.create(Enumerable, (function() { * * The order of the values is not guaranteed. * - *
Example
+ * ##### Example * * var h = $H({one: "uno", two: "due", three: "tre"}); * h.values(); @@ -220,12 +243,12 @@ var Hash = Class.create(Enumerable, (function() { * - object (Object | Hash): The object to merge with this hash to produce * the resulting hash. * - * Returns a new `Hash` instance with `object`'s key/value pairs merged in; + * Returns a new [[Hash]] instance with `object`'s key/value pairs merged in; * this hash remains unchanged. * * To modify the original hash in place, use [[Hash#update]]. * - *
Example
+ * ##### Example * * var h = $H({one: "uno", two: "due"}); * var h2 = h.merge({three: "tre"}); @@ -246,10 +269,10 @@ var Hash = Class.create(Enumerable, (function() { * Updates a hash *in place* with the key/value pairs of `object`, returns * the hash. * - * `update` modifies the hash. To get a new hash instead, use + * [[Hash#update]] modifies the hash. To get a new hash instead, use * [[Hash#merge]]. * - *
Example
+ * ##### Example * * var h = $H({one: "uno", two: "due"}); * h.update({three: "tre"}); @@ -267,7 +290,17 @@ var Hash = Class.create(Enumerable, (function() { // Private. No PDoc necessary. function toQueryPair(key, value) { if (Object.isUndefined(value)) return key; - return key + '=' + encodeURIComponent(String.interpret(value)); + + value = String.interpret(value); + + // Normalize newlines as \r\n because the HTML spec says newlines should + // be encoded as CRLFs. + value = value.gsub(/(\r)?\n/, '\r\n'); + value = encodeURIComponent(value); + // Likewise, according to the spec, spaces should be '+' rather than + // '%20'. + value = value.gsub(/%20/, '+'); + return key + '=' + value; } /** related to: String#toQueryParams @@ -289,7 +322,7 @@ var Hash = Class.create(Enumerable, (function() { * The order of pairs in the string is not guaranteed, other than the order * of array values described above. * - *
Example
+ * ##### Example * * $H({action: 'ship', * order_id: 123, @@ -312,10 +345,21 @@ var Hash = Class.create(Enumerable, (function() { function toQueryString() { return this.inject([], function(results, pair) { var key = encodeURIComponent(pair.key), values = pair.value; - + if (values && typeof values == 'object') { - if (Object.isArray(values)) - return results.concat(values.map(toQueryPair.curry(key))); + if (Object.isArray(values)) { + // We used to use `Array#map` here to get the query pair for each + // item in the array, but that caused test regressions once we + // added the sparse array behavior for array iterator methods. + // Changed to an ordinary `for` loop so that we can handle + // `undefined` values ourselves rather than have them skipped. + var queryValues = []; + for (var i = 0, len = values.length, value; i < len; i++) { + value = values[i]; + queryValues.push(toQueryPair(key, value)); + } + return results.concat(queryValues); + } } else results.push(toQueryPair(key, values)); return results; }).join('&'); @@ -332,25 +376,10 @@ var Hash = Class.create(Enumerable, (function() { }).join(', ') + '}>'; } - /** related to: Object.toJSON - * Hash#toJSON() -> String - * - * Returns a JSON string containing the keys and values in this hash. - * - *
Example
- * - * var h = $H({'a': 'apple', 'b': 23, 'c': false}); - * h.toJSON(); - * // -> {"a": "apple", "b": 23, "c": false} - **/ - function toJSON() { - return Object.toJSON(this.toObject()); - } - - /** + /** * Hash#clone() -> Hash * - * Returns a clone of this Hash. + * Returns a clone of this [[Hash]]. **/ function clone() { return new Hash(this); @@ -371,7 +400,7 @@ var Hash = Class.create(Enumerable, (function() { update: update, toQueryString: toQueryString, inspect: inspect, - toJSON: toJSON, + toJSON: toObject, clone: clone }; })()); diff --git a/src/lang/number.js b/src/prototype/lang/number.js similarity index 90% rename from src/lang/number.js rename to src/prototype/lang/number.js index 459d9dca6..f6acbc8de 100644 --- a/src/lang/number.js +++ b/src/prototype/lang/number.js @@ -20,7 +20,7 @@ Object.extend(Number.prototype, (function() { * (which is therefore assumed to be in the \[0..255\] range, inclusive). * Useful for composing CSS color strings. * - *
Example
+ * ##### Example * * 10.toColorPart() * // -> "0a" @@ -32,8 +32,8 @@ Object.extend(Number.prototype, (function() { /** * Number#succ() -> Number * - * Returns the successor of the current Number, as defined by current + 1. - * Used to make numbers compatible with ObjectRange. + * Returns the successor of the current [[Number]], as defined by current + 1. + * Used to make numbers compatible with [[ObjectRange]]. **/ function succ() { return this + 1; @@ -49,7 +49,7 @@ Object.extend(Number.prototype, (function() { * the first parameter. The number will be 0 on first call, 1 on second * call, etc. `times` returns the number instance it was called on. * - *
Example
+ * ##### Example * * (3).times(alert); * // -> Alerts "0", then "1", then "2"; returns 3 @@ -81,7 +81,7 @@ Object.extend(Number.prototype, (function() { * that the string's length is at least equal to `length`. Takes an optional * `radix` argument which specifies the base to use for conversion. * - *
Examples
+ * ##### Examples * * (13).toPaddedString(4); * // -> "0013" @@ -103,15 +103,6 @@ Object.extend(Number.prototype, (function() { return '0'.times(length - string.length) + string; } - /** related to: Object.toJSON - * Number#toJSON() -> String - * - * Returns a JSON string representation of the number. - **/ - function toJSON() { - return isFinite(this) ? this.toString() : 'null'; - } - /** * Number#abs() -> Number * @@ -159,7 +150,6 @@ Object.extend(Number.prototype, (function() { succ: succ, times: times, toPaddedString: toPaddedString, - toJSON: toJSON, abs: abs, round: round, ceil: ceil, diff --git a/src/prototype/lang/object.js b/src/prototype/lang/object.js new file mode 100644 index 000000000..47fba60fd --- /dev/null +++ b/src/prototype/lang/object.js @@ -0,0 +1,537 @@ +/** section: Language + * class Object + * + * Extensions to the built-in [[Object]] object. + * + * Because it is dangerous and invasive to augment `Object.prototype` (i.e., + * add instance methods to objects), all these methods are static methods that + * take an [[Object]] as their first parameter. + * + * [[Object]] is used by Prototype as a namespace; that is, it just keeps a few + * new methods together, which are intended for namespaced access (i.e. starting + * with "`Object.`"). + * + * For the regular developer (who simply uses Prototype without tweaking it), the + * most commonly used methods are probably [[Object.inspect]] and, to a lesser degree, + * [[Object.clone]]. + * + * Advanced users, who wish to create their own objects like Prototype does, or + * explore objects as if they were hashes, will turn to [[Object.extend]], + * [[Object.keys]], and [[Object.values]]. +**/ +(function() { + + var _toString = Object.prototype.toString, + NULL_TYPE = 'Null', + UNDEFINED_TYPE = 'Undefined', + BOOLEAN_TYPE = 'Boolean', + NUMBER_TYPE = 'Number', + STRING_TYPE = 'String', + OBJECT_TYPE = 'Object', + FUNCTION_CLASS = '[object Function]', + BOOLEAN_CLASS = '[object Boolean]', + NUMBER_CLASS = '[object Number]', + STRING_CLASS = '[object String]', + ARRAY_CLASS = '[object Array]', + DATE_CLASS = '[object Date]'; + + function Type(o) { + switch(o) { + case null: return NULL_TYPE; + case (void 0): return UNDEFINED_TYPE; + } + var type = typeof o; + switch(type) { + case 'boolean': return BOOLEAN_TYPE; + case 'number': return NUMBER_TYPE; + case 'string': return STRING_TYPE; + } + return OBJECT_TYPE; + } + + /** + * Object.extend(destination, source) -> Object + * - destination (Object): The object to receive the new properties. + * - source (Object): The object whose properties will be duplicated. + * + * Copies all properties from the source to the destination object. Used by Prototype + * to simulate inheritance (rather statically) by copying to prototypes. + * + * Documentation should soon become available that describes how Prototype implements + * OOP, where you will find further details on how Prototype uses [[Object.extend]] and + * [[Class.create]] (something that may well change in version 2.0). It will be linked + * from here. + * + * Do not mistake this method with its quasi-namesake [[Element.extend]], + * which implements Prototype's (much more complex) DOM extension mechanism. + **/ + function extend(destination, source) { + for (var property in source) + destination[property] = source[property]; + return destination; + } + + /** + * Object.inspect(obj) -> String + * - object (Object): The item to be inspected. + * + * Returns the debug-oriented string representation of the object. + * + * * `undefined` and `null` are represented as such. + * * Other types are looked up for a `inspect` method: if there is one, it is used, otherwise, + * it reverts to the `toString` method. + * + * Prototype provides `inspect` methods for many types, both built-in and library-defined, + * such as in [[String#inspect]], [[Array#inspect]], [[Enumerable#inspect]] and [[Hash#inspect]], + * which attempt to provide most-useful string representations (from a developer's standpoint) + * for their respective types. + * + * ##### Examples + * + * Object.inspect(); + * // -> 'undefined' + * + * Object.inspect(null); + * // -> 'null' + * + * Object.inspect(false); + * // -> 'false' + * + * Object.inspect([1, 2, 3]); + * // -> '[1, 2, 3]' + * + * Object.inspect('hello'); + * // -> "'hello'" + **/ + function inspect(object) { + try { + if (isUndefined(object)) return 'undefined'; + if (object === null) return 'null'; + return object.inspect ? object.inspect() : String(object); + } catch (e) { + if (e instanceof RangeError) return '...'; + throw e; + } + } + + /** + * Object.toJSON(object) -> String + * - object (Object): The object to be serialized. + * + * Returns a JSON string. + * + * `undefined` and `function` types have no JSON representation. `boolean` + * and `null` are coerced to strings. + * + * For other types, [[Object.toJSON]] looks for a `toJSON` method on `object`. + * If there is one, it is used; otherwise the object is treated like a + * generic [[Object]]. + * + * For more information on Prototype's JSON encoder, hop to our + * [tutorial](http://prototypejs.org/learn/json). + * + * ##### Example + * + * var data = {name: 'Violet', occupation: 'character', age: 25, pets: ['frog', 'rabbit']}; + * Object.toJSON(data); + * //-> '{"name": "Violet", "occupation": "character", "age": 25, "pets": ["frog","rabbit"]}' + **/ + function toJSON(value) { + return JSON.stringify(value); + } + + function Str(key, holder, stack) { + var value = holder[key]; + if (Type(value) === OBJECT_TYPE && typeof value.toJSON === 'function') { + value = value.toJSON(key); + } + + var _class = _toString.call(value); + + switch (_class) { + case NUMBER_CLASS: + case BOOLEAN_CLASS: + case STRING_CLASS: + value = value.valueOf(); + } + + switch (value) { + case null: return 'null'; + case true: return 'true'; + case false: return 'false'; + } + + var type = typeof value; + switch (type) { + case 'string': + return value.inspect(true); + case 'number': + return isFinite(value) ? String(value) : 'null'; + case 'object': + + for (var i = 0, length = stack.length; i < length; i++) { + if (stack[i] === value) { + throw new TypeError("Cyclic reference to '" + value + "' in object"); + } + } + stack.push(value); + + var partial = []; + if (_class === ARRAY_CLASS) { + for (var i = 0, length = value.length; i < length; i++) { + var str = Str(i, value, stack); + partial.push(typeof str === 'undefined' ? 'null' : str); + } + partial = '[' + partial.join(',') + ']'; + } else { + var keys = Object.keys(value); + for (var i = 0, length = keys.length; i < length; i++) { + var key = keys[i], str = Str(key, value, stack); + if (typeof str !== "undefined") { + partial.push(key.inspect(true)+ ':' + str); + } + } + partial = '{' + partial.join(',') + '}'; + } + stack.pop(); + return partial; + } + } + + /** + * Object.toQueryString(object) -> String + * - object (Object): The object whose property/value pairs will be converted. + * + * Turns an object into its URL-encoded query string representation. + * + * This is a form of serialization, and is mostly useful to provide complex + * parameter sets for stuff such as objects in the [[Ajax]] namespace (e.g. + * [[Ajax.Request]]). + * + * Undefined-value pairs will be serialized as if empty-valued. Array-valued + * pairs will get serialized with one name/value pair per array element. All + * values get URI-encoded using JavaScript's native `encodeURIComponent` + * function. + * + * The order of pairs in the serialized form is not guaranteed (and mostly + * irrelevant anyway) — except for array-based parts, which are serialized + * in array order. + * + * ##### Examples + * + * Object.toQueryString({ action: 'ship', order_id: 123, fees: ['f1', 'f2'], 'label': 'a demo' }) + * // -> 'action=ship&order_id=123&fees=f1&fees=f2&label=a+demo' + **/ + function toQueryString(object) { + return $H(object).toQueryString(); + } + + /** + * Object.toHTML(object) -> String + * - object (Object): The object to convert to HTML. + * + * Converts the object to its HTML representation. + * + * Returns the return value of `object`'s `toHTML` method if it exists; else + * runs `object` through [[String.interpret]]. + * + * ##### Examples + * + * var Bookmark = Class.create({ + * initialize: function(name, url) { + * this.name = name; + * this.url = url; + * }, + * + * toHTML: function() { + * return '#{name}'.interpolate(this); + * } + * }); + * + * var api = new Bookmark('Prototype API', 'http://prototypejs.org/api'); + * + * Object.toHTML(api); + * //-> 'Prototype API' + * + * Object.toHTML("Hello world!"); + * //-> "Hello world!" + * + * Object.toHTML(); + * //-> "" + * + * Object.toHTML(null); + * //-> "" + * + * Object.toHTML(undefined); + * //-> "" + * + * Object.toHTML(true); + * //-> "true" + * + * Object.toHTML(false); + * //-> "false" + * + * Object.toHTML(123); + * //-> "123" + **/ + function toHTML(object) { + return object && object.toHTML ? object.toHTML() : String.interpret(object); + } + + /** + * Object.values(object) -> Array + * - object (Object): The object to pull values from. + * + * Returns an array of the object's property values. + * + * Note that the order of the resulting array is browser-dependent — it + * relies on the `for...in` loop, for which the ECMAScript spec does not + * prescribe an enumeration order. + * + * Also, remember that while property _names_ are unique, property _values_ + * have no such constraint. + * + * ##### Examples + * + * Object.values(); + * // -> [] + * + * Object.values({ name: 'Prototype', version: '1.6.1' }).sort(); + * // -> ['1.6.1', 'Prototype'] + **/ + function values(object) { + var results = []; + for (var property in object) + results.push(object[property]); + return results; + } + + /** + * Object.clone(object) -> Object + * - object (Object): The object to clone. + * + * Creates and returns a shallow duplicate of the passed object by copying + * all of the original's key/value pairs onto an empty object. + * + * Do note that this is a _shallow_ copy, not a _deep_ copy. Nested objects + * will retain their references. + * + * ##### Examples + * + * var original = {name: 'primaryColors', values: ['red', 'green', 'blue']}; + * var copy = Object.clone(original); + * + * original.name; + * // -> "primaryColors" + * original.values[0]; + * // -> "red" + * copy.name; + * // -> "primaryColors" + * + * copy.name = "secondaryColors"; + * original.name; + * // -> "primaryColors" + * copy.name; + * // -> "secondaryColors" + * + * copy.values[0] = 'magenta'; + * copy.values[1] = 'cyan'; + * copy.values[2] = 'yellow'; + * original.values[0]; + * // -> "magenta" (it's a shallow copy, so they share the array) + **/ + function clone(object) { + return extend({ }, object); + } + + /** + * Object.isElement(object) -> Boolean + * - object (Object): The object to test. + * + * Returns `true` if `object` is a DOM node of type 1; `false` otherwise. + * + * ##### Examples + * + * Object.isElement(new Element('div')); + * //-> true + * + * Object.isElement(document.createElement('div')); + * //-> true + * + * Object.isElement($('id_of_an_exiting_element')); + * //-> true + * + * Object.isElement(document.createTextNode('foo')); + * //-> false + **/ + function isElement(object) { + return !!(object && object.nodeType == 1); + } + + /** + * Object.isArray(object) -> Boolean + * - object (Object): The object to test. + * + * Returns `true` if `object` is an [[Array]]; `false` otherwise. + * + * ##### Examples + * + * Object.isArray([]); + * //-> true + * + * Object.isArray($w()); + * //-> true + * + * Object.isArray({ }); + * //-> false + **/ + var isArray = Array.isArray; + + /** + * Object.isHash(object) -> Boolean + * - object (Object): The object to test. + * + * Returns `true` if `object` is an instance of the [[Hash]] class; `false` + * otherwise. + * + * ##### Examples + * + * Object.isHash(new Hash({ })); + * //-> true + * + * Object.isHash($H({ })); + * //-> true + * + * Object.isHash({ }); + * //-> false + **/ + function isHash(object) { + return object instanceof Hash; + } + + /** + * Object.isFunction(object) -> Boolean + * - object (Object): The object to test. + * + * Returns `true` if `object` is of type [[Function]]; `false` otherwise. + * + * ##### Examples + * + * Object.isFunction($); + * //-> true + * + * Object.isFunction(123); + * //-> false + **/ + function isFunction(object) { + return _toString.call(object) === FUNCTION_CLASS; + } + + /** + * Object.isString(object) -> Boolean + * - object (Object): The object to test. + * + * Returns `true` if `object` is of type [[String]]; `false` otherwise. + * + * ##### Examples + * + * Object.isString("foo"); + * //-> true + * + * Object.isString(""); + * //-> true + * + * Object.isString(123); + * //-> false + **/ + function isString(object) { + return _toString.call(object) === STRING_CLASS; + } + + /** + * Object.isNumber(object) -> Boolean + * - object (Object): The object to test. + * + * Returns `true` if `object` is of type [[Number]]; `false` otherwise. + * + * ##### Examples + * + * Object.isNumber(0); + * //-> true + * + * Object.isNumber(1.2); + * //-> true + * + * Object.isNumber("foo"); + * //-> false + **/ + function isNumber(object) { + return _toString.call(object) === NUMBER_CLASS; + } + + /** + * Object.isDate(object) -> Boolean + * - object (Object): The object to test. + * + * Returns `true` if `object` is of type [[Date]]; `false` otherwise. + * + * ##### Examples + * + * Object.isDate(new Date); + * //-> true + * + * Object.isDate("Dec 25, 1995"); + * //-> false + * + * Object.isDate(new Date("Dec 25, 1995")); + * //-> true + **/ + function isDate(object) { + return _toString.call(object) === DATE_CLASS; + } + + /** + * Object.isUndefined(object) -> Boolean + * - object (Object): The object to test. + * + * Returns `true` if `object` is of type `undefined`; `false` otherwise. + * + * ##### Examples + * + * Object.isUndefined(); + * //-> true + * + * Object.isUndefined(undefined); + * //-> true + * + * Object.isUndefined(null); + * //-> false + * + * Object.isUndefined(0); + * //-> false + * + * Object.isUndefined(""); + * //-> false + **/ + function isUndefined(object) { + return typeof object === "undefined"; + } + + extend(Object, { + extend: extend, + inspect: inspect, + toJSON: toJSON, + toQueryString: toQueryString, + toHTML: toHTML, + values: values, + clone: clone, + isArray: isArray, + isElement: isElement, + isHash: isHash, + isFunction: isFunction, + isString: isString, + isNumber: isNumber, + isDate: isDate, + isUndefined: isUndefined + }); +})(); diff --git a/src/lang/periodical_executer.js b/src/prototype/lang/periodical_executer.js similarity index 57% rename from src/lang/periodical_executer.js rename to src/prototype/lang/periodical_executer.js index c4232621d..519cabd43 100644 --- a/src/lang/periodical_executer.js +++ b/src/prototype/lang/periodical_executer.js @@ -1,24 +1,32 @@ /** section: Language * class PeriodicalExecuter * - * A class that oversees the calling of a particular function periodically. + * Oversees the calling of a particular function periodically. * - * `PeriodicalExecuter` shields you from multiple parallel executions of the + * [[PeriodicalExecuter]] shields you from multiple parallel executions of a * `callback` function, should it take longer than the given interval to * execute. * * This is especially useful if you use one to interact with the user at * given intervals (e.g. use a prompt or confirm call): this will avoid * multiple message boxes all waiting to be actioned. + * + * ##### Example + * + * new PeriodicalExecuter(function(pe) { + * if (!confirm('Want me to annoy you again later?')) { + * pe.stop(); + * } + * }, 5); **/ var PeriodicalExecuter = Class.create({ /** * new PeriodicalExecuter(callback, frequency) * - callback (Function): the function to be executed at each interval. - * - frequency (Number): the amount of time, in sections, to wait in between - * callbacks. + * - frequency (Number): the amount of time, in seconds, to wait in between + * callbacks. * - * Creates an `PeriodicalExecuter`. + * Creates a [[PeriodicalExecuter]]. **/ initialize: function(callback, frequency) { this.callback = callback; @@ -40,6 +48,23 @@ var PeriodicalExecuter = Class.create({ * PeriodicalExecuter#stop() -> undefined * * Stops the periodical executer (there will be no further triggers). + * + * Once a [[PeriodicalExecuter]] is created, it constitues an infinite loop, + * triggering at the given interval until the page unloads. This method lets + * you stop it any time you want. + * + * ##### Example + * + * This will only alert 1, 2 and 3, then the [[PeriodicalExecuter]] stops. + * + * var count = 0; + * new PeriodicalExecuter(function(pe) { + * if (++count > 3) { + * pe.stop(); + * } else { + * alert(count); + * } + * }, 1); **/ stop: function() { if (!this.timer) return; @@ -49,18 +74,11 @@ var PeriodicalExecuter = Class.create({ onTimerEvent: function() { if (!this.currentlyExecuting) { - // IE doesn't support `finally` statements unless all errors are caught. - // We mimic the behaviour of `finally` statements by duplicating code - // that would belong in it. First at the bottom of the `try` statement - // (for errorless cases). Secondly, inside a `catch` statement which - // rethrows any caught errors. try { this.currentlyExecuting = true; this.execute(); + } finally { this.currentlyExecuting = false; - } catch(e) { - this.currentlyExecuting = false; - throw e; } } } diff --git a/src/prototype/lang/range.js b/src/prototype/lang/range.js new file mode 100644 index 000000000..931542f78 --- /dev/null +++ b/src/prototype/lang/range.js @@ -0,0 +1,157 @@ +/** section: Language + * class ObjectRange + * includes Enumerable + * + * A succession of values. + * + * An [[ObjectRange]] can model a range of any value that implements a `succ` + * method (which links that value to its "successor"). + * + * Prototype provides such a method for [[Number]] and [[String]], but you + * are (of course) welcome to implement useful semantics in your own objects, + * in order to enable ranges based on them. + * + * [[ObjectRange]] mixes in [[Enumerable]], which makes ranges very versatile. + * It takes care, however, to override the default code for `include`, to + * achieve better efficiency. + * + * While [[ObjectRange]] does provide a constructor, the preferred way to obtain + * a range is to use the [[$R]] utility function, which is strictly equivalent + * (only way more concise to use). + * + * See [[$R]] for more information. +**/ + +/** section: Language + * $R(start, end[, exclusive = false]) -> ObjectRange + * + * Creates a new [[ObjectRange]] object. This method is a convenience wrapper + * around the [[ObjectRange]] constructor, but [[$R]] is the preferred alias. + * + * [[ObjectRange]] instances represent a range of consecutive values, be they + * numerical, textual, or of another type that semantically supports value + * ranges. See the type's documentation for further details, and to discover + * how your own objects can support value ranges. + * + * The [[$R]] function takes exactly the same arguments as the original + * constructor: the **lower and upper bounds** (value of the same, proper + * type), and **whether the upper bound is exclusive** or not. By default, the + * upper bound is inclusive. + * + * ##### Examples + * + * $R(0, 10).include(10) + * // -> true + * + * $A($R(0, 5)).join(', ') + * // -> '0, 1, 2, 3, 4, 5' + * + * $A($R('aa', 'ah')).join(', ') + * // -> 'aa, ab, ac, ad, ae, af, ag, ah' + * + * $R(0, 10, true).include(10) + * // -> false + * + * $R(0, 10, true).each(function(value) { + * // invoked 10 times for value = 0 to 9 + * }); + * + * Note that [[ObjectRange]] mixes in the [[Enumerable]] module: this makes it + * easy to convert a range to an [[Array]] ([[Enumerable]] provides the + * [[Enumerable#toArray]] method, which makes the [[$A]] conversion + * straightforward), or to iterate through values. (Note, however, that getting + * the bounds back will be more efficiently done using the + * [[ObjectRange#start]] and [[ObjectRange#end]] properties than calling the + * [[Enumerable#min]] and [[Enumerable#max]] methods). + * + * ##### Warning + * + * **Be careful with [[String]] ranges**: as described in its [[String#succ]] + * method, it does not use alphabetical boundaries, but goes all the way + * through the character table: + * + * $A($R('a', 'e')) + * // -> ['a', 'b', 'c', 'd', 'e'], no surprise there + * + * $A($R('ax', 'ba')) + * // -> Ouch! Humongous array, starting as ['ax', 'ay', 'az', 'a{', 'a|', 'a}', 'a~'...] + * + * See [[ObjectRange]] for more information. +**/ +function $R(start, end, exclusive) { + return new ObjectRange(start, end, exclusive); +} + +var ObjectRange = Class.create(Enumerable, (function() { + /** + * new ObjectRange(start, end[, exclusive = false]) + * + * Creates a new [[ObjectRange]]. + * + * The `exclusive` argument specifies whether `end` itself is a part of the + * range. + **/ + function initialize(start, end, exclusive) { + /** + * ObjectRange#start -> ? + * + * The lower bounding value of the range. + **/ + this.start = start; + /** + * ObjectRange#end -> ? + * + * The upper bounding value of the range. + **/ + this.end = end; + this.exclusive = exclusive; + } + + function _each(iterator, context) { + var value = this.start, i; + for (i = 0; this.include(value); i++) { + iterator.call(context, value, i); + value = value.succ(); + } + } + + /** + * ObjectRange#include(value) -> Boolean + * + * Determines whether the value is included in the range. + * + * This assumes the values in the range have a valid strict weak ordering + * (have valid semantics for the `<` operator). While [[ObjectRange]] mixes + * in [[Enumerable]], this method overrides the default version of + * [[Enumerable#include]], and is much more efficient (it uses a maximum of + * two comparisons). + * + * ##### Examples + * + * $R(1, 10).include(5); + * // -> true + * + * $R('a', 'h').include('x'); + * // -> false + * + * $R(1, 10).include(10); + * // -> true + * + * $R(1, 10, true).include(10); + * // -> false + **/ + function include(value) { + if (value < this.start) + return false; + if (this.exclusive) + return value < this.end; + return value <= this.end; + } + + return { + initialize: initialize, + _each: _each, + include: include + }; +})()); + diff --git a/src/lang/regexp.js b/src/prototype/lang/regexp.js similarity index 73% rename from src/lang/regexp.js rename to src/prototype/lang/regexp.js index fd7c04a99..c1663408b 100644 --- a/src/lang/regexp.js +++ b/src/prototype/lang/regexp.js @@ -4,10 +4,12 @@ * Extensions to the built-in `RegExp` object. **/ -/** alias of: RegExp#test +/** * RegExp#match(str) -> Boolean + * - str (String): a string against witch to match the regular expression. * - * Return true if string matches the regular expression, false otherwise. + * Alias of the native `RegExp#test` method. Returns `true` + * if `str` matches the regular expression, `false` otherwise. **/ RegExp.prototype.match = RegExp.prototype.test; diff --git a/src/prototype/lang/string.js b/src/prototype/lang/string.js new file mode 100644 index 000000000..433a1c0b1 --- /dev/null +++ b/src/prototype/lang/string.js @@ -0,0 +1,927 @@ +/** section: Language + * class String + * + * Extensions to the built-in `String` class. + * + * Prototype enhances the [[String]] object with a series of useful methods for + * ranging from the trivial to the complex. Tired of stripping trailing + * whitespace? Try [[String#strip]]. Want to replace `replace`? Have a look at + * [[String#sub]] and [[String#gsub]]. Need to parse a query string? We have + * [[String#toQueryParams what you need]]. +**/ +Object.extend(String, { + /** + * String.interpret(value) -> String + * + * Coerces `value` into a string. Returns an empty string for `null`. + **/ + interpret: function(value) { + return value == null ? '' : String(value); + }, + specialChar: { + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '\\': '\\\\' + } +}); + +Object.extend(String.prototype, (function() { + + function prepareReplacement(replacement) { + if (Object.isFunction(replacement)) return replacement; + var template = new Template(replacement); + return function(match) { return template.evaluate(match) }; + } + + // In some versions of Chrome, an empty RegExp has "(?:)" as a `source` + // property instead of an empty string. + function isNonEmptyRegExp(regexp) { + return regexp.source && regexp.source !== '(?:)'; + } + + + /** + * String#gsub(pattern, replacement) -> String + * + * Returns the string with _every_ occurence of a given pattern replaced by either a + * regular string, the returned value of a function or a [[Template]] string. + * The pattern can be a string or a regular expression. + * + * If its second argument is a string [[String#gsub]] works just like the native JavaScript + * method `replace()` set to global match. + * + * var mouseEvents = 'click dblclick mousedown mouseup mouseover mousemove mouseout'; + * + * mouseEvents.gsub(' ', ', '); + * // -> 'click, dblclick, mousedown, mouseup, mouseover, mousemove, mouseout' + * + * mouseEvents.gsub(/\s+/, ', '); + * // -> 'click, dblclick, mousedown, mouseup, mouseover, mousemove, mouseout' + * + * If you pass it a function, it will be invoked for every occurrence of the pattern + * with the match of the current pattern as its unique argument. Note that this argument + * is the returned value of the `match()` method called on the current pattern. It is + * in the form of an array where the first element is the entire match and every subsequent + * one corresponds to a parenthesis group in the regex. + * + * mouseEvents.gsub(/\w+/, function(match){ return 'on' + match[0].capitalize() }); + * // -> 'onClick onDblclick onMousedown onMouseup onMouseover onMousemove onMouseout' + * + * var markdown = '![a pear](/img/pear.jpg) ![an orange](/img/orange.jpg)'; + * + * markdown.gsub(/!\[(.*?)\]\((.*?)\)/, function(match) { + * return '' + match[1] + ''; + * }); + * // -> 'a pear an orange' + * + * Lastly, you can pass [[String#gsub]] a [[Template]] string in which you can also access + * the returned value of the `match()` method using the ruby inspired notation: `#{0}` + * for the first element of the array, `#{1}` for the second one, and so on. + * So our last example could be easily re-written as: + * + * markdown.gsub(/!\[(.*?)\]\((.*?)\)/, '#{1}'); + * // -> 'a pear an orange' + * + * If you need an equivalent to [[String#gsub]] but without global match set on, try [[String#sub]]. + * + * ##### Note + * + * Do _not_ use the `"g"` flag on the regex as this will create an infinite loop. + **/ + function gsub(pattern, replacement) { + var result = '', source = this, match; + replacement = prepareReplacement(replacement); + + if (Object.isString(pattern)) + pattern = RegExp.escape(pattern); + + if (!(pattern.length || isNonEmptyRegExp(pattern))) { + replacement = replacement(''); + return replacement + source.split('').join(replacement) + replacement; + } + + while (source.length > 0) { + match = source.match(pattern) + if (match && match[0].length > 0) { + result += source.slice(0, match.index); + result += String.interpret(replacement(match)); + source = source.slice(match.index + match[0].length); + } else { + result += source, source = ''; + } + } + return result; + } + + /** + * String#sub(pattern, replacement[, count = 1]) -> String + * + * Returns a string with the _first_ `count` occurrences of `pattern` replaced by either + * a regular string, the returned value of a function or a [[Template]] string. + * `pattern` can be a string or a regular expression. + * + * Unlike [[String#gsub]], [[String#sub]] takes a third optional parameter which specifies + * the number of occurrences of the pattern which will be replaced. + * If not specified, it will default to 1. + * + * Apart from that, [[String#sub]] works just like [[String#gsub]]. + * Please refer to it for a complete explanation. + * + * ##### Examples + * + * var fruits = 'apple pear orange'; + * + * fruits.sub(' ', ', '); + * // -> 'apple, pear orange' + * + * fruits.sub(' ', ', ', 1); + * // -> 'apple, pear orange' + * + * fruits.sub(' ', ', ', 2); + * // -> 'apple, pear, orange' + * + * fruits.sub(/\w+/, function(match){ return match[0].capitalize() + ',' }, 2); + * // -> 'Apple, Pear, orange' + * + * var markdown = '![a pear](/img/pear.jpg) ![an orange](/img/orange.jpg)'; + * + * markdown.sub(/!\[(.*?)\]\((.*?)\)/, function(match) { + * return '' + match[1] + ''; + * }); + * // -> 'a pear ![an orange](/img/orange.jpg)' + * + * markdown.sub(/!\[(.*?)\]\((.*?)\)/, '#{1}'); + * // -> 'a pear ![an orange](/img/orange.jpg)' + * + * ##### Note + * + * Do _not_ use the `"g"` flag on the regex as this will create an infinite loop. + **/ + function sub(pattern, replacement, count) { + replacement = prepareReplacement(replacement); + count = Object.isUndefined(count) ? 1 : count; + + return this.gsub(pattern, function(match) { + if (--count < 0) return match[0]; + return replacement(match); + }); + } + + /** related to: String#gsub + * String#scan(pattern, iterator) -> String + * + * Allows iterating over every occurrence of the given pattern (which can be a + * string or a regular expression). + * Returns the original string. + * + * Internally just calls [[String#gsub]] passing it `pattern` and `iterator` as arguments. + * + * ##### Examples + * + * 'apple, pear & orange'.scan(/\w+/, alert); + * // -> 'apple pear & orange' (and displays 'apple', 'pear' and 'orange' in three successive alert dialogs) + * + * Can be used to populate an array: + * + * var fruits = []; + * 'apple, pear & orange'.scan(/\w+/, function(match) { fruits.push(match[0]) }); + * fruits.inspect() + * // -> ['apple', 'pear', 'orange'] + * + * or even to work on the DOM: + * + * 'failure-message, success-message & spinner'.scan(/(\w|-)+/, Element.toggle) + * // -> 'failure-message, success-message & spinner' (and toggles the visibility of each DOM element) + * + * ##### Note + * + * Do _not_ use the `"g"` flag on the regex as this will create an infinite loop. + **/ + function scan(pattern, iterator) { + this.gsub(pattern, iterator); + return String(this); + } + + /** + * String#truncate([length = 30[, suffix = '...']]) -> String + * + * Truncates a string to given `length` and appends `suffix` to it (indicating + * that it is only an excerpt). + * + * ##### Examples + * + * 'A random sentence whose length exceeds 30 characters.'.truncate(); + * // -> 'A random sentence whose len...' + * + * 'Some random text'.truncate(); + * // -> 'Some random text.' + * + * 'Some random text'.truncate(10); + * // -> 'Some ra...' + * + * 'Some random text'.truncate(10, ' [...]'); + * // -> 'Some [...]' + **/ + function truncate(length, truncation) { + length = length || 30; + truncation = Object.isUndefined(truncation) ? '...' : truncation; + return this.length > length ? + this.slice(0, length - truncation.length) + truncation : String(this); + } + + /** + * String#strip() -> String + * + * Strips all leading and trailing whitespace from a string. + * + * ##### Example + * + * ' hello world! '.strip(); + * // -> 'hello world!' + **/ + function strip() { + return this.replace(/^\s+/, '').replace(/\s+$/, ''); + } + + /** + * String#stripTags() -> String + * + * Strips a string of any HTML tags. + * + * Note that [[String#stripTags]] will only strip HTML 4.01 tags — like + * `div`, `span`, and `abbr`. It _will not_ strip namespace-prefixed tags + * such as `h:table` or `xsl:template`. + * + * Watch out for `'.stripTags(); + * // -> 'a linkalert("hello world!");' + * + * 'a link'.stripScripts().stripTags(); + * // -> 'a link' + **/ + function stripTags() { + return this.replace(/<\w+(\s+("[^"]*"|'[^']*'|[^>])+)?(\/)?>|<\/\w+>/gi, ''); + } + + /** + * String#stripScripts() -> String + * + * Strips a string of things that look like HTML script blocks. + * + * ##### Example + * + * "

This is a test.End of test

".stripScripts(); + * // => "

This is a test.End of test

" + * + * ##### Caveat User + * + * Note that the processing [[String#stripScripts]] does is good enough for + * most purposes, but you cannot rely on it for security purposes. If you're + * processing end-user-supplied content, [[String#stripScripts]] is probably + * not sufficiently robust to prevent hack attacks. + **/ + function stripScripts() { + return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), ''); + } + + /** + * String#extractScripts() -> Array + * + * Extracts the content of any `'.extractScripts(); + * // -> ['2 + 2'] + * + * ''.extractScripts(); + * // -> ['2 + 2', 'alert("hello world!")'] + * + * ##### Notes + * + * To evaluate the scripts later on, you can use the following: + * + * var myScripts = ''.extractScripts(); + * // -> ['2 + 2', 'alert("hello world!")'] + * + * var myReturnedValues = myScripts.map(function(script) { + * return eval(script); + * }); + * // -> [4, undefined] (and displays 'hello world!' in the alert dialog) + **/ + function extractScripts() { + var matchAll = new RegExp(Prototype.ScriptFragment, 'img'), + matchOne = new RegExp(Prototype.ScriptFragment, 'im'); + var matchMimeType = new RegExp(Prototype.ExecutableScriptFragment, 'im'); + var matchTypeAttribute = /type=/i; + + var results = []; + (this.match(matchAll) || []).each(function(scriptTag) { + var match = scriptTag.match(matchOne); + var attributes = match[1]; + if (attributes !== '') { + // If the script has a `type` attribute, make sure it has a + // JavaScript MIME-type. If not, ignore it. + attributes = attributes.strip(); + var hasTypeAttribute = (matchTypeAttribute).test(attributes); + var hasMimeType = (matchMimeType).test(attributes); + if (hasTypeAttribute && !hasMimeType) return; + } + results.push(match ? match[2] : ''); + }); + + return results; + } + + /** + * String#evalScripts() -> Array + * + * Evaluates the content of any inline `'.evalScripts(); + * // -> [4] + * + * ''.evalScripts(); + * // -> [4, undefined] (and displays 'hello world!' in the alert dialog) + * + * ##### About `evalScripts`, `var`s, and defining functions + * + * [[String#evalScripts]] evaluates script blocks, but this **does not** mean + * they are evaluated in the global scope. They aren't, they're evaluated in + * the scope of the [[String#evalScripts]] method. This has important + * ramifications for your scripts: + * + * * Anything in your script declared with the `var` keyword will be + * discarded momentarily after evaluation, and will be invisible to any + * other scope. + * * If any ` - - -
- - - - - -
- - <%= @content_for_layout %> - -
- - - -
- - - diff --git a/templates/html/namespace.erb b/templates/html/namespace.erb deleted file mode 100644 index 5e5725637..000000000 --- a/templates/html/namespace.erb +++ /dev/null @@ -1,171 +0,0 @@ -<% d = @doc_instance %> - -<% @title = "#{d.full_name} #{d.type}" %> - -<%= include "partials/breadcrumbs", :object => d %> - -

- <%= d.type %> <%= d.full_name %> -

- -<% # Does it have a description? %> - -<% if d.description && !d.description.empty? %> -
- -
-

Description

-
- -
- <%= htmlize(d.description) %> -
- -
-<% end %> - -<% # Is it a CLASS? %> -<% if d.is_a?(Documentation::Klass) %> - - <% if d.superklass %> -
-
-

Superclass

-
- -
-

<%= auto_link_code(d.superklass, false) %>

-
-
- <% end %> - - <% unless d.subklasses.empty? %> -
-
-

Subclasses

-
- -
-

<%= d.subklasses.map { |s| auto_link_code(s, false) }.join(', ') %>

-
-
- <% end %> - -<% end %> - -<% # Does it have MIXINS? %> -<% unless d.mixins.compact.empty? %> -
-
-

Includes

-
- -
-

<%= d.mixins.map { |m| auto_link_code(m, false) }.join(', ') %>

-
-
-<% end %> - -<% # Are there related utilities? %> -<% unless d.related_utilities.empty? %> -
-
-

Related utilities

-
- -
-

<%= d.related_utilities.map { |u| auto_link_code(u, false) }.join(', ') %>

-
-
-<% end %> - -<% # List its methods. %> -<% unless d.all_methods.empty? && d.mixins.empty? %> -
-
-

Methods

-
- -
-
    - <% d.all_methods.each do |method| %> -
  • <%= auto_link_code(method, true, :class => class_names_for(method)) %>
  • - <% end %> -
- - <% unless d.mixins.compact.empty? %> - <% d.mixins.each do |mixin| %> -

Inherited from <%= auto_link(mixin) %>

-
    - <% mixin.all_methods.each do |method| %> -
  • <%= auto_link_code(method, true, :class => class_names_for(method)) %>
  • - <% end %> -
- <% end %> - <% end %> - - -
- -
-<% end %> - -<% if d.is_a?(Documentation::Klass) && d.constructor %> -
-
-

Constructor

-
- -
-
<%= d.constructor.ebnf_expressions.join("\n").strip %>
- <% unless d.constructor.arguments.empty? %> -
    - <% d.constructor.arguments.each do |arg| %> -
  • - <%= arg.name %> - <% unless arg.types.empty? %> - (<%= arg.types.map { |t| auto_link_code(t, false) }.join(' | ') %>) - <% end %> - <%= ' – ' + inline_htmlize(arg.description) unless arg.description.empty? %> -
  • - <% end %> -
- <% end %> - - -

<%= htmlize(d.constructor.description) %>

- -
- -
-<% end %> - - -<% -types = { - :constants => "Constants", - :klass_methods => "Class methods", - :klass_properties => "Class properties", - :instance_methods => "Instance methods", - :instance_properties => "Instance properties" -} -%> - -<% types.each do |method, title| %> - <% methods = d.send(method) %> - <% unless methods.empty? %> -
- -
-

<%= title %>

-
- -
-
    - <%= include "partials/short_description", :collection => methods %> -
- -
-
- <% end %> -<% end %> \ No newline at end of file diff --git a/templates/html/partials/breadcrumbs.erb b/templates/html/partials/breadcrumbs.erb deleted file mode 100644 index 7e02719d4..000000000 --- a/templates/html/partials/breadcrumbs.erb +++ /dev/null @@ -1,6 +0,0 @@ - \ No newline at end of file diff --git a/templates/html/partials/short_description.erb b/templates/html/partials/short_description.erb deleted file mode 100644 index 73d3faba0..000000000 --- a/templates/html/partials/short_description.erb +++ /dev/null @@ -1,48 +0,0 @@ -
  • -

    <%= object.name %> - #

    - - <% if object.signature %> - <%= method_synopsis(object) %> - <% end %> - - <% if object.is_a?(Documentation::Method) %> - <% unless object.arguments.empty? %> -
      - <% object.arguments.each do |arg| %> -
    • - <%= arg.name %> - <% unless arg.types.empty? %> - (<%= arg.types.map { |t| auto_link_code(t, false) }.join(' | ') %>) - <% end %> - <%= ' – ' + inline_htmlize(arg.description) unless arg.description.empty? %> -
    • - <% end %> -
    - <% end %> - <% end %> - - <%= htmlize(object.description) %> - - <% # Does it have an alias? %> - <% unless object.aliases.empty? %> -

    Aliased as: <%= object.aliases.map { |a| auto_link_code(a, false) }.join(', ') %>

    - <% end %> - - <% # Is it an alias of something else? %> - <% if object.alias? %> -

    Alias of: <%= auto_link_code(object.alias_of, false) %>

    - <% end %> - - <% # Is it related to something? %> - <% if object.is_a?(Documentation::Utility) && object.related_to %> - - <% end %> - - <% # Is it methodized? %> - <% if object.is_a?(Documentation::Method) && object.methodized? %> -

    This method can be called either as an instance method or as a generic method. If calling as a generic, pass the instance in as the first argument.

    - <% end %> - -
  • diff --git a/templates/html/section.erb b/templates/html/section.erb deleted file mode 100644 index f433573bd..000000000 --- a/templates/html/section.erb +++ /dev/null @@ -1,99 +0,0 @@ -<% @title = "#{@doc_instance.full_name}" %> -<% section = @doc_instance %> - -<%= include "partials/breadcrumbs", :object => section %> - -

    - Section <%= section.name %> -

    - -
    - -
    -

    Description

    -
    - -
    - <%= htmlize(section.description) %> -
    - -
    - -<% # Iterate over the items in this section. %> - -<% utilities = section.utilities %> -<% unless utilities.empty? %> - -
    -
    -

    Utilities

    -
    - -
    -
      - <% utilities.each do |utility| %> -
    • <%= auto_link_code(utility) %>
    • - <% end %> -
    -
    -
    - -<% end %> - -<% namespaces = section.namespaces %> -<% unless namespaces.empty? %> - -
    -
    -

    Namespaces

    -
    - -
    -
      - <% namespaces.each do |namespace| %> -
    • -

      - <%= namespace.full_name %> -

      - - <% unless namespace.short_description.nil? %> -

      <%= htmlize(namespace.short_description) %>

      - <% end %> - -
    • - <% end %> -
    -
    -
    - -<% end %> - -<% klasses = section.klasses %> -<% unless klasses.empty? %> - -
    - -
    -

    Classes

    -
    - -
    -
      - <% klasses.each do |klass| %> -
    • -

      - <%= klass.full_name %> -

      - - <% unless klass.short_description.nil? %> -

      <%= htmlize(klass.short_description) %>

      - <% end %> - -
    • - <% end %> -
    -
    - -
    - -<% end %> \ No newline at end of file diff --git a/templates/html/utility.erb b/templates/html/utility.erb deleted file mode 100644 index 3ffc98d24..000000000 --- a/templates/html/utility.erb +++ /dev/null @@ -1,21 +0,0 @@ -<% d = @doc_instance %> - -<% @title = "#{@doc_instance.full_name} utility" %> - -<%= include "partials/breadcrumbs", :object => d %> - -

    - utility <%= @doc_instance.name %> -

    - -
    -
    -

    Description

    -
    - -
    -
      - <%= include "partials/short_description", :collection => [@doc_instance] %> -
    -
    -
    \ No newline at end of file diff --git a/test/functional/event.html b/test/functional/event.html index 77c2a3243..4e2d11d3d 100644 --- a/test/functional/event.html +++ b/test/functional/event.html @@ -5,25 +5,38 @@ Prototype functional test file - + - + - +

    Scope test - scope of the handler should be this element

    - +

    Event object test - should be present as a first argument

    - +

    Hijack link test (preventDefault)

    - + - +

    Mouse click: left middle right

    - + - +

    Context menu event (tries to prevent default)

    - +

    Event.element() test

    - +

    Event.currentTarget test

    - + - +

    Event.findElement() test

    - + - +

    Stop propagation test (bubbling)

    - + - +

    Keyup test - focus on the textarea and type

    - + - +

    bindAsEventListener() test

    - + - +

    Object.inspect(event) test

    - + - +

    mouseenter test

    - + - +

    Add unload events

    - + + +
    + Event delegation +
      +
    • + Child 1 (click) +
    • +
    • + Child 2 (mouseover) +
    • +
    • + Child 3 (mouseup) +
    • +
    + + Results: + +
      +
    • Test 1
    • +
    • Test 2
    • +
    • Test 3
    • +
    +
    + + + + diff --git a/test/unit/ajax_test.js b/test/unit/ajax_test.js deleted file mode 100644 index 7848a83be..000000000 --- a/test/unit/ajax_test.js +++ /dev/null @@ -1,379 +0,0 @@ -var extendDefault = function(options) { - return Object.extend({ - asynchronous: false, - method: 'get', - onException: function(e) { throw e } - }, options); -}; - -new Test.Unit.Runner({ - setup: function() { - $('content').update(''); - $('content2').update(''); - }, - - teardown: function() { - // hack to cleanup responders - Ajax.Responders.responders = [Ajax.Responders.responders[0]]; - }, - - testSynchronousRequest: function() { - this.assertEqual("", $("content").innerHTML); - - this.assertEqual(0, Ajax.activeRequestCount); - new Ajax.Request("../fixtures/hello.js", { - asynchronous: false, - method: 'GET', - evalJS: 'force' - }); - this.assertEqual(0, Ajax.activeRequestCount); - - var h2 = $("content").firstChild; - this.assertEqual("Hello world!", h2.innerHTML); - }, - - testAsynchronousRequest: function() { - this.assertEqual("", $("content").innerHTML); - - new Ajax.Request("../fixtures/hello.js", { - asynchronous: true, - method: 'get', - evalJS: 'force' - }); - this.wait(1000, function() { - var h2 = $("content").firstChild; - this.assertEqual("Hello world!", h2.innerHTML); - }); - }, - - testUpdater: function() { - this.assertEqual("", $("content").innerHTML); - - new Ajax.Updater("content", "../fixtures/content.html", { method:'get' }); - - this.wait(1000, function() { - this.assertEqual(sentence, $("content").innerHTML.strip().toLowerCase()); - - $('content').update(''); - this.assertEqual("", $("content").innerHTML); - - new Ajax.Updater({ success:"content", failure:"content2" }, - "../fixtures/content.html", { method:'get', parameters:{ pet:'monkey' } }); - - new Ajax.Updater("", "../fixtures/content.html", { method:'get', parameters:"pet=monkey" }); - - this.wait(1000, function() { - this.assertEqual(sentence, $("content").innerHTML.strip().toLowerCase()); - this.assertEqual("", $("content2").innerHTML); - }); - }); - }, - - testUpdaterWithInsertion: function() { - $('content').update(); - new Ajax.Updater("content", "../fixtures/content.html", { method:'get', insertion: Insertion.Top }); - this.wait(1000, function() { - this.assertEqual(sentence, $("content").innerHTML.strip().toLowerCase()); - $('content').update(); - new Ajax.Updater("content", "../fixtures/content.html", { method:'get', insertion: 'bottom' }); - this.wait(1000, function() { - this.assertEqual(sentence, $("content").innerHTML.strip().toLowerCase()); - - $('content').update(); - new Ajax.Updater("content", "../fixtures/content.html", { method:'get', insertion: 'after' }); - this.wait(1000, function() { - this.assertEqual('five dozen', $("content").next().innerHTML.strip().toLowerCase()); - }); - }); - }); - }, - - testUpdaterOptions: function() { - var options = { - method: 'get', - asynchronous: false, - evalJS: 'force', - onComplete: Prototype.emptyFunction - } - var request = new Ajax.Updater("content", "../fixtures/hello.js", options); - request.options.onComplete = Prototype.emptyFunction; - this.assertIdentical(Prototype.emptyFunction, options.onComplete); - }, - - testResponders: function(){ - // check for internal responder - this.assertEqual(1, Ajax.Responders.responders.length); - - var dummyResponder = { - onComplete: Prototype.emptyFunction - }; - - Ajax.Responders.register(dummyResponder); - this.assertEqual(2, Ajax.Responders.responders.length); - - // don't add twice - Ajax.Responders.register(dummyResponder); - this.assertEqual(2, Ajax.Responders.responders.length); - - Ajax.Responders.unregister(dummyResponder); - this.assertEqual(1, Ajax.Responders.responders.length); - - var responder = { - onCreate: function(req){ responderCounter++ }, - onLoading: function(req){ responderCounter++ }, - onComplete: function(req){ responderCounter++ } - }; - Ajax.Responders.register(responder); - - this.assertEqual(0, responderCounter); - this.assertEqual(0, Ajax.activeRequestCount); - new Ajax.Request("../fixtures/content.html", { method:'get', parameters:"pet=monkey" }); - this.assertEqual(1, responderCounter); - this.assertEqual(1, Ajax.activeRequestCount); - - this.wait(1000,function() { - this.assertEqual(3, responderCounter); - this.assertEqual(0, Ajax.activeRequestCount); - }); - }, - - testEvalResponseShouldBeCalledBeforeOnComplete: function() { - if (this.isRunningFromRake) { - this.assertEqual("", $("content").innerHTML); - - this.assertEqual(0, Ajax.activeRequestCount); - new Ajax.Request("../fixtures/hello.js", extendDefault({ - onComplete: function(response) { this.assertNotEqual("", $("content").innerHTML) }.bind(this) - })); - this.assertEqual(0, Ajax.activeRequestCount); - - var h2 = $("content").firstChild; - this.assertEqual("Hello world!", h2.innerHTML); - } else { - this.info(message); - } - }, - - testContentTypeSetForSimulatedVerbs: function() { - if (this.isRunningFromRake) { - new Ajax.Request('/inspect', extendDefault({ - method: 'put', - contentType: 'application/bogus', - onComplete: function(response) { - this.assertEqual('application/bogus; charset=UTF-8', response.responseJSON.headers['content-type']); - }.bind(this) - })); - } else { - this.info(message); - } - }, - - testOnCreateCallback: function() { - new Ajax.Request("../fixtures/content.html", extendDefault({ - onCreate: function(transport) { this.assertEqual(0, transport.readyState) }.bind(this), - onComplete: function(transport) { this.assertNotEqual(0, transport.readyState) }.bind(this) - })); - }, - - testEvalJS: function() { - if (this.isRunningFromRake) { - - $('content').update(); - new Ajax.Request("/response", extendDefault({ - parameters: Fixtures.js, - onComplete: function(transport) { - var h2 = $("content").firstChild; - this.assertEqual("Hello world!", h2.innerHTML); - }.bind(this) - })); - - $('content').update(); - new Ajax.Request("/response", extendDefault({ - evalJS: false, - parameters: Fixtures.js, - onComplete: function(transport) { - this.assertEqual("", $("content").innerHTML); - }.bind(this) - })); - } else { - this.info(message); - } - - $('content').update(); - new Ajax.Request("../fixtures/hello.js", extendDefault({ - evalJS: 'force', - onComplete: function(transport) { - var h2 = $("content").firstChild; - this.assertEqual("Hello world!", h2.innerHTML); - }.bind(this) - })); - }, - - testCallbacks: function() { - var options = extendDefault({ - onCreate: function(transport) { this.assertInstanceOf(Ajax.Response, transport) }.bind(this) - }); - - Ajax.Request.Events.each(function(state){ - options['on' + state] = options.onCreate; - }); - - new Ajax.Request("../fixtures/content.html", options); - }, - - testResponseText: function() { - new Ajax.Request("../fixtures/empty.html", extendDefault({ - onComplete: function(transport) { this.assertEqual('', transport.responseText) }.bind(this) - })); - - new Ajax.Request("../fixtures/content.html", extendDefault({ - onComplete: function(transport) { this.assertEqual(sentence, transport.responseText.toLowerCase()) }.bind(this) - })); - }, - - testResponseXML: function() { - if (this.isRunningFromRake) { - new Ajax.Request("/response", extendDefault({ - parameters: Fixtures.xml, - onComplete: function(transport) { - this.assertEqual('foo', transport.responseXML.getElementsByTagName('name')[0].getAttribute('attr')) - }.bind(this) - })); - } else { - this.info(message); - } - }, - - testResponseJSON: function() { - if (this.isRunningFromRake) { - new Ajax.Request("/response", extendDefault({ - parameters: Fixtures.json, - onComplete: function(transport) { this.assertEqual(123, transport.responseJSON.test) }.bind(this) - })); - - new Ajax.Request("/response", extendDefault({ - parameters: { - 'Content-Length': 0, - 'Content-Type': 'application/json' - }, - onComplete: function(transport) { this.assertNull(transport.responseJSON) }.bind(this) - })); - - new Ajax.Request("/response", extendDefault({ - evalJSON: false, - parameters: Fixtures.json, - onComplete: function(transport) { this.assertNull(transport.responseJSON) }.bind(this) - })); - - new Ajax.Request("/response", extendDefault({ - parameters: Fixtures.jsonWithoutContentType, - onComplete: function(transport) { this.assertNull(transport.responseJSON) }.bind(this) - })); - - new Ajax.Request("/response", extendDefault({ - sanitizeJSON: true, - parameters: Fixtures.invalidJson, - onException: function(request, error) { - this.assert(error.message.include('Badly formed JSON string')); - this.assertInstanceOf(Ajax.Request, request); - }.bind(this) - })); - } else { - this.info(message); - } - - new Ajax.Request("../fixtures/data.json", extendDefault({ - evalJSON: 'force', - onComplete: function(transport) { this.assertEqual(123, transport.responseJSON.test) }.bind(this) - })); - }, - - testHeaderJSON: function() { - if (this.isRunningFromRake) { - new Ajax.Request("/response", extendDefault({ - parameters: Fixtures.headerJson, - onComplete: function(transport, json) { - this.assertEqual('hello #éà', transport.headerJSON.test); - this.assertEqual('hello #éà', json.test); - }.bind(this) - })); - - new Ajax.Request("/response", extendDefault({ - onComplete: function(transport, json) { - this.assertNull(transport.headerJSON) - this.assertNull(json) - }.bind(this) - })); - } else { - this.info(message); - } - }, - - testGetHeader: function() { - if (this.isRunningFromRake) { - new Ajax.Request("/response", extendDefault({ - parameters: { 'X-TEST': 'some value' }, - onComplete: function(transport) { - this.assertEqual('some value', transport.getHeader('X-Test')); - this.assertNull(transport.getHeader('X-Inexistant')); - }.bind(this) - })); - } else { - this.info(message); - } - }, - - testParametersCanBeHash: function() { - if (this.isRunningFromRake) { - new Ajax.Request("/response", extendDefault({ - parameters: $H({ "one": "two", "three": "four" }), - onComplete: function(transport) { - this.assertEqual("two", transport.getHeader("one")); - this.assertEqual("four", transport.getHeader("three")); - this.assertNull(transport.getHeader("toObject")); - }.bind(this) - })); - } else { - this.info(message); - } - }, - - testIsSameOriginMethod: function() { - var isSameOrigin = Ajax.Request.prototype.isSameOrigin; - this.assert(isSameOrigin.call({ url: '/foo/bar.html' }), '/foo/bar.html'); - this.assert(isSameOrigin.call({ url: window.location.toString() }), window.location); - this.assert(!isSameOrigin.call({ url: 'http://example.com' }), 'http://example.com'); - - if (this.isRunningFromRake) { - Ajax.Request.prototype.isSameOrigin = function() { - return false - }; - - $("content").update('same origin policy'); - new Ajax.Request("/response", extendDefault({ - parameters: Fixtures.js, - onComplete: function(transport) { - this.assertEqual("same origin policy", $("content").innerHTML); - }.bind(this) - })); - - new Ajax.Request("/response", extendDefault({ - parameters: Fixtures.invalidJson, - onException: function(request, error) { - this.assert(error.message.include('Badly formed JSON string')); - }.bind(this) - })); - - new Ajax.Request("/response", extendDefault({ - parameters: { 'X-JSON': '{});window.attacked = true;({}' }, - onException: function(request, error) { - this.assert(error.message.include('Badly formed JSON string')); - }.bind(this) - })); - - Ajax.Request.prototype.isSameOrigin = isSameOrigin; - } else { - this.info(message); - } - } -}); diff --git a/test/unit/array_test.js b/test/unit/array_test.js deleted file mode 100644 index 7fa07b09c..000000000 --- a/test/unit/array_test.js +++ /dev/null @@ -1,196 +0,0 @@ -var globalArgsTest = 'nothing to see here'; - -new Test.Unit.Runner({ - test$A: function(){ - this.assertEnumEqual([], $A({})); - }, - - testToArrayOnArguments: function(){ - function toArrayOnArguments(){ - globalArgsTest = $A(arguments); - } - toArrayOnArguments(); - this.assertEnumEqual([], globalArgsTest); - toArrayOnArguments('foo'); - this.assertEnumEqual(['foo'], globalArgsTest); - toArrayOnArguments('foo','bar'); - this.assertEnumEqual(['foo','bar'], globalArgsTest); - }, - - testToArrayOnNodeList: function(){ - // direct HTML - this.assertEqual(3, $A($('test_node').childNodes).length); - - // DOM - var element = document.createElement('div'); - element.appendChild(document.createTextNode('22')); - (2).times(function(){ element.appendChild(document.createElement('span')) }); - this.assertEqual(3, $A(element.childNodes).length); - - // HTML String - element = document.createElement('div'); - $(element).update('22 -1) { - this.info('Running on WebKit'); - this.assert(Prototype.Browser.WebKit); - } - - if(Object.prototype.toString.call(window.opera) === '[object Opera]') { - this.info('Running on Opera'); - this.assert(Prototype.Browser.Opera); - } - - if(!!(window.attachEvent && !window.opera)) { - this.info('Running on IE'); - this.assert(Prototype.Browser.IE); - } - - if(navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1) { - this.info('Running on Gecko'); - this.assert(Prototype.Browser.Gecko); - } - } -}); \ No newline at end of file diff --git a/test/unit/browsers.sample.yml b/test/unit/browsers.sample.yml new file mode 100644 index 000000000..86cb549ad --- /dev/null +++ b/test/unit/browsers.sample.yml @@ -0,0 +1,9 @@ +# +# If the test runner doesn't know where your browsers are, you can define +# them here. +# + +browsers: + firefox: C:\Program Files\Mozilla Firefox\firefox.exe + opera: C:\Program Files\Opera\launcher.exe + chrome: C:\Program Files\Google\Chrome\Application\chrome.exe diff --git a/test/unit/caja_whitelists/html_attrib.json b/test/unit/caja_whitelists/html_attrib.json deleted file mode 100644 index 3b025ed79..000000000 --- a/test/unit/caja_whitelists/html_attrib.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "description": "Extends the default HTML attribute whiteList for Prototype testing purposes.", - - "inherits": [ - { - "src": "resource:///html4-attributes.json" - } - ], - - "allowed": [ - "FORM:ACCEPT-CHARSET", - "DIV:FOO", - "DIV:BAR", - "A:ACCESSKEY" - ], - - "types": [ - { - "key": "A:ACCESSKEY", - "description": "Allow the A:ACCESSKEY accessibility key character", - "pattern": ".?", - "optional": true - }, - - { - "key": "INPUT:TYPE", - "description": "Overwrite INPUT:TYPE rule to allow for DATE value.", - "pattern": "TEXT|PASSWORD|CHECKBOX|RADIO|SUBMIT|RESET|FILE|HIDDEN|IMAGE|BUTTON|DATE", - "optional": true - }, - - { - "key": "A:TABINDEX", - "description": "Overwrite A:TABINDEX to allow for an empty value", - "pattern": ".*", - "optional": true - }, - - { - "key": "DIV:FOO", - "description": "Custom BAR attribute.", - "pattern": ".*", - "optional": true - }, - - { - "key": "DIV:BAR", - "description": "Custom FOO attribute", - "pattern": ".*", - "optional": true - } - ] -} diff --git a/test/unit/class_test.js b/test/unit/class_test.js deleted file mode 100644 index 8c5e9c393..000000000 --- a/test/unit/class_test.js +++ /dev/null @@ -1,136 +0,0 @@ -new Test.Unit.Runner({ - testClassCreate: function() { - this.assert(Object.isFunction(Animal), 'Animal is not a constructor'); - this.assertEnumEqual([Cat, Mouse, Dog, Ox], Animal.subclasses); - Animal.subclasses.each(function(subclass) { - this.assertEqual(Animal, subclass.superclass); - }, this); - - var Bird = Class.create(Animal); - this.assertEqual(Bird, Animal.subclasses.last()); - // for..in loop (for some reason) doesn't iterate over the constructor property in top-level classes - this.assertEnumEqual(Object.keys(new Animal).sort(), Object.keys(new Bird).without('constructor').sort()); - }, - - testClassInstantiation: function() { - var pet = new Animal("Nibbles"); - this.assertEqual("Nibbles", pet.name, "property not initialized"); - this.assertEqual('Nibbles: Hi!', pet.say('Hi!')); - this.assertEqual(Animal, pet.constructor, "bad constructor reference"); - this.assertUndefined(pet.superclass); - - var Empty = Class.create(); - this.assert('object', typeof new Empty); - }, - - testInheritance: function() { - var tom = new Cat('Tom'); - this.assertEqual(Cat, tom.constructor, "bad constructor reference"); - this.assertEqual(Animal, tom.constructor.superclass, 'bad superclass reference'); - this.assertEqual('Tom', tom.name); - this.assertEqual('Tom: meow', tom.say('meow')); - this.assertEqual('Tom: Yuk! I only eat mice.', tom.eat(new Animal)); - }, - - testSuperclassMethodCall: function() { - var tom = new Cat('Tom'); - this.assertEqual('Tom: Yum!', tom.eat(new Mouse)); - - // augment the constructor and test - var Dodo = Class.create(Animal, { - initialize: function($super, name) { - $super(name); - this.extinct = true; - }, - - say: function($super, message) { - return $super(message) + " honk honk"; - } - }); - - var gonzo = new Dodo('Gonzo'); - this.assertEqual('Gonzo', gonzo.name); - this.assert(gonzo.extinct, 'Dodo birds should be extinct'); - this.assertEqual("Gonzo: hello honk honk", gonzo.say("hello")); - }, - - testClassAddMethods: function() { - var tom = new Cat('Tom'); - var jerry = new Mouse('Jerry'); - - Animal.addMethods({ - sleep: function() { - return this.say('ZZZ'); - } - }); - - Mouse.addMethods({ - sleep: function($super) { - return $super() + " ... no, can't sleep! Gotta steal cheese!"; - }, - escape: function(cat) { - return this.say('(from a mousehole) Take that, ' + cat.name + '!'); - } - }); - - this.assertEqual('Tom: ZZZ', tom.sleep(), "added instance method not available to subclass"); - this.assertEqual("Jerry: ZZZ ... no, can't sleep! Gotta steal cheese!", jerry.sleep()); - this.assertEqual("Jerry: (from a mousehole) Take that, Tom!", jerry.escape(tom)); - // insure that a method has not propagated *up* the prototype chain: - this.assertUndefined(tom.escape); - this.assertUndefined(new Animal().escape); - - Animal.addMethods({ - sleep: function() { - return this.say('zZzZ'); - } - }); - - this.assertEqual("Jerry: zZzZ ... no, can't sleep! Gotta steal cheese!", jerry.sleep()); - }, - - testBaseClassWithMixin: function() { - var grass = new Plant('grass', 3); - this.assertRespondsTo('getValue', grass); - this.assertEqual('#', grass.inspect()); - }, - - testSubclassWithMixin: function() { - var snoopy = new Dog('Snoopy', 12, 'male'); - this.assertRespondsTo('reproduce', snoopy); - }, - - testSubclassWithMixins: function() { - var cow = new Ox('cow', 400, 'female'); - this.assertEqual('#', cow.inspect()); - this.assertRespondsTo('reproduce', cow); - this.assertRespondsTo('getValue', cow); - }, - - testClassWithToStringAndValueOfMethods: function() { - var Foo = Class.create({ - toString: function() { return "toString" }, - valueOf: function() { return "valueOf" } - }); - - var Bar = Class.create(Foo, { - valueOf: function() { return "myValueOf" } - }); - - var Parent = Class.create({ - m1: function(){ return 'm1' }, - m2: function(){ return 'm2' } - }); - var Child = Class.create(Parent, { - m1: function($super) { return 'm1 child' }, - m2: function($super) { return 'm2 child' } - }); - - this.assert(new Child().m1.toString().indexOf('m1 child') > -1); - - this.assertEqual("toString", new Foo().toString()); - this.assertEqual("valueOf", new Foo().valueOf()); - this.assertEqual("toString", new Bar().toString()); - this.assertEqual("myValueOf", new Bar().valueOf()); - } -}); \ No newline at end of file diff --git a/test/unit/config.ru b/test/unit/config.ru new file mode 100644 index 000000000..fbdeac151 --- /dev/null +++ b/test/unit/config.ru @@ -0,0 +1,2 @@ +require './server' +run UnitTests \ No newline at end of file diff --git a/test/unit/date_test.js b/test/unit/date_test.js deleted file mode 100644 index 6cd68c32b..000000000 --- a/test/unit/date_test.js +++ /dev/null @@ -1,5 +0,0 @@ -new Test.Unit.Runner({ - testDateToJSON: function() { - this.assertEqual('\"1970-01-01T00:00:00Z\"', new Date(Date.UTC(1970, 0, 1)).toJSON()); - } -}); \ No newline at end of file diff --git a/test/unit/dom_test.js b/test/unit/dom_test.js deleted file mode 100644 index 32995845c..000000000 --- a/test/unit/dom_test.js +++ /dev/null @@ -1,1510 +0,0 @@ -var getInnerHTML = function(id) { - return $(id).innerHTML.toString().toLowerCase().gsub(/[\r\n\t]/, ''); -}; -var createParagraph = function(text) { - var p = document.createElement('p'); - p.appendChild(document.createTextNode(text)); - return p; -} - -new Test.Unit.Runner({ - setup: function() { - if (documentViewportProperties) return; - // Based on properties check from http://www.quirksmode.org/viewport/compatibility.html - documentViewportProperties = { - properties : [ - 'self.pageXOffset', 'self.pageYOffset', - 'self.screenX', 'self.screenY', - 'self.innerHeight', 'self.innerWidth', - 'self.outerHeight', 'self.outerWidth', - 'self.screen.height', 'self.screen.width', - 'self.screen.availHeight', 'self.screen.availWidth', - 'self.screen.availTop', 'self.screen.availLeft', - 'self.screen.Top', 'self.screen.Left', - 'self.screenTop', 'self.screenLeft', - 'document.body.clientHeight', 'document.body.clientWidth', - 'document.body.scrollHeight', 'document.body.scrollWidth', - 'document.body.scrollLeft', 'document.body.scrollTop', - 'document.body.offsetHeight', 'document.body.offsetWidth', - 'document.body.offsetTop', 'document.body.offsetLeft' - ].inject([], function(properties, prop) { - if(!self.screen && prop.include('self.screen')) return; - if (!document.body && prop.include('document.body')) return; - properties.push(prop); - if (prop.include('.body') && document.documentElement) - properties.push(prop.sub('.body', '.documentElement')); - return properties; - }), - - inspect : function() { - var props = []; - this.properties.each(function(prop) { - if (eval(prop)) props[prop] = eval(prop); - }, this); - return props; - } - }; - }, - - testDollarFunction: function() { - this.assertUndefined($()); - - this.assertNull(document.getElementById('noWayThisIDExists')); - this.assertNull($('noWayThisIDExists')); - - this.assertIdentical(document.getElementById('testdiv'), $('testdiv')); - this.assertEnumEqual([ $('testdiv'), $('container') ], $('testdiv', 'container')); - this.assertEnumEqual([ $('testdiv'), undefined, $('container') ], - $('testdiv', 'noWayThisIDExists', 'container')); - var elt = $('testdiv'); - this.assertIdentical(elt, $(elt)); - this.assertRespondsTo('hide', elt); - this.assertRespondsTo('childOf', elt); - }, - - testGetElementsByClassName: function() { - if (document.getElementsByClassName.toString().include('[native code]')) { - this.info("browser uses native getElementsByClassName; skipping tests"); - return; - } - - - var div = $('class_names'), list = $('class_names_ul'); - - this.assertElementsMatch(document.getElementsByClassName('A'), 'p.A', 'ul#class_names_ul.A', 'li.A.C'); - - - var isElementPrototypeSupported = (function(){ - var el = document.createElement('div'); - var result = typeof el.show != 'undefined'; - el = null; - return result; - })(); - - if (!isElementPrototypeSupported) - this.assertUndefined(document.getElementById('unextended').show); - - this.assertElementsMatch(div.getElementsByClassName('B'), 'ul#class_names_ul.A.B', 'div.B.C.D'); - this.assertElementsMatch(div.getElementsByClassName('D C B'), 'div.B.C.D'); - this.assertElementsMatch(div.getElementsByClassName(' D\nC\tB '), 'div.B.C.D'); - this.assertElementsMatch(div.getElementsByClassName($w('D C B'))); - this.assertElementsMatch(list.getElementsByClassName('A'), 'li.A.C'); - this.assertElementsMatch(list.getElementsByClassName(' A '), 'li.A.C'); - this.assertElementsMatch(list.getElementsByClassName('C A'), 'li.A.C'); - this.assertElementsMatch(list.getElementsByClassName("C\nA "), 'li.A.C'); - this.assertElementsMatch(list.getElementsByClassName('B')); - this.assertElementsMatch(list.getElementsByClassName('1'), 'li.1'); - this.assertElementsMatch(list.getElementsByClassName([1]), 'li.1'); - this.assertElementsMatch(list.getElementsByClassName(['1 junk'])); - this.assertElementsMatch(list.getElementsByClassName('')); - this.assertElementsMatch(list.getElementsByClassName(' ')); - this.assertElementsMatch(list.getElementsByClassName([''])); - this.assertElementsMatch(list.getElementsByClassName([' ', ''])); - this.assertElementsMatch(list.getElementsByClassName({})); - - // those lookups shouldn't have extended all nodes in document - if (!isElementPrototypeSupported) - this.assertUndefined(document.getElementById('unextended')['show']); - }, - - testElementInsertWithHTML: function() { - Element.insert('insertions-main', {before:'

    before text

    more testing

    '}); - this.assert(getInnerHTML('insertions-container').startsWith('

    before text

    more testing

    ')); - Element.insert('insertions-main', {after:'

    after text

    more testing

    '}); - this.assert(getInnerHTML('insertions-container').endsWith('

    after text

    more testing

    ')); - Element.insert('insertions-main', {top:'

    top text.

    more testing

    '}); - this.assert(getInnerHTML('insertions-main').startsWith('

    top text.

    more testing

    ')); - Element.insert('insertions-main', {bottom:'

    bottom text.

    more testing

    '}); - this.assert(getInnerHTML('insertions-main').endsWith('

    bottom text.

    more testing

    ')); - }, - - testElementInsertWithDOMNode: function() { - Element.insert('insertions-node-main', {before: createParagraph('node before')}); - this.assert(getInnerHTML('insertions-node-container').startsWith('

    node before

    ')); - Element.insert('insertions-node-main', {after: createParagraph('node after')}); - this.assert(getInnerHTML('insertions-node-container').endsWith('

    node after

    ')); - Element.insert('insertions-node-main', {top:createParagraph('node top')}); - this.assert(getInnerHTML('insertions-node-main').startsWith('

    node top

    ')); - Element.insert('insertions-node-main', {bottom:createParagraph('node bottom')}); - this.assert(getInnerHTML('insertions-node-main').endsWith('

    node bottom

    ')); - this.assertEqual($('insertions-node-main'), $('insertions-node-main').insert(document.createElement('p'))); - }, - - testElementInsertWithToElementMethod: function() { - Element.insert('insertions-node-main', {toElement: createParagraph.curry('toElement') }); - this.assert(getInnerHTML('insertions-node-main').endsWith('

    toelement

    ')); - Element.insert('insertions-node-main', {bottom: {toElement: createParagraph.curry('bottom toElement') }}); - this.assert(getInnerHTML('insertions-node-main').endsWith('

    bottom toelement

    ')); - }, - - testElementInsertWithToHTMLMethod: function() { - Element.insert('insertions-node-main', {toHTML: function() { return '

    toHTML

    '} }); - this.assert(getInnerHTML('insertions-node-main').endsWith('

    tohtml

    ')); - Element.insert('insertions-node-main', {bottom: {toHTML: function() { return '

    bottom toHTML

    '} }}); - this.assert(getInnerHTML('insertions-node-main').endsWith('

    bottom tohtml

    ')); - }, - - testElementInsertWithNonString: function() { - Element.insert('insertions-main', {bottom:3}); - this.assert(getInnerHTML('insertions-main').endsWith('3')); - }, - - testElementInsertInTables: function() { - Element.insert('second_row', {after:'Third Row'}); - this.assert($('second_row').descendantOf('table')); - - $('a_cell').insert({top:'hello world'}); - this.assert($('a_cell').innerHTML.startsWith('hello world')); - $('a_cell').insert({after:'hi planet'}); - this.assertEqual('hi planet', $('a_cell').next().innerHTML); - $('table_for_insertions').insert('a cell!'); - this.assert($('table_for_insertions').innerHTML.gsub('\r\n', '').toLowerCase().include('a cell!')); - $('row_1').insert({after:'last'}); - this.assertEqual('last', $A($('table_for_row_insertions').getElementsByTagName('tr')).last().lastChild.innerHTML); - }, - - testElementInsertInSelect: function() { - var selectTop = $('select_for_insert_top'), selectBottom = $('select_for_insert_bottom'); - selectBottom.insert(''); - this.assertEqual('option 45', selectBottom.getValue()); - selectTop.insert({top:''}); - this.assertEqual(4, selectTop.options.length); - }, - - testElementMethodInsert: function() { - $('element-insertions-main').insert({before:'some text before'}); - this.assert(getInnerHTML('element-insertions-container').startsWith('some text before')); - $('element-insertions-main').insert({after:'some text after'}); - this.assert(getInnerHTML('element-insertions-container').endsWith('some text after')); - $('element-insertions-main').insert({top:'some text top'}); - this.assert(getInnerHTML('element-insertions-main').startsWith('some text top')); - $('element-insertions-main').insert({bottom:'some text bottom'}); - this.assert(getInnerHTML('element-insertions-main').endsWith('some text bottom')); - - $('element-insertions-main').insert('some more text at the bottom'); - this.assert(getInnerHTML('element-insertions-main').endsWith('some more text at the bottom')); - - $('element-insertions-main').insert({TOP:'some text uppercase top'}); - this.assert(getInnerHTML('element-insertions-main').startsWith('some text uppercase top')); - - $('element-insertions-multiple-main').insert({ - top:'1', bottom:2, before: new Element('p').update('3'), after:'4' - }); - this.assert(getInnerHTML('element-insertions-multiple-main').startsWith('1')); - this.assert(getInnerHTML('element-insertions-multiple-main').endsWith('2')); - this.assert(getInnerHTML('element-insertions-multiple-container').startsWith('

    3

    ')); - this.assert(getInnerHTML('element-insertions-multiple-container').endsWith('4')); - - $('element-insertions-main').update('test'); - $('element-insertions-main').insert(null); - $('element-insertions-main').insert({bottom:null}); - this.assertEqual('test', getInnerHTML('element-insertions-main')); - $('element-insertions-main').insert(1337); - this.assertEqual('test1337', getInnerHTML('element-insertions-main')); - }, - - testNewElementInsert: function() { - var container = new Element('div'), element = new Element('div'); - container.insert(element); - - element.insert({ before: '

    a paragraph

    ' }); - this.assertEqual('

    a paragraph

    ', getInnerHTML(container)); - element.insert({ after: 'some text' }); - this.assertEqual('

    a paragraph

    some text', getInnerHTML(container)); - - element.insert({ top: '

    a paragraph

    ' }); - this.assertEqual('

    a paragraph

    ', getInnerHTML(element)); - element.insert('some text'); - this.assertEqual('

    a paragraph

    some text', getInnerHTML(element)); - }, - - testInsertionBackwardsCompatibility: function() { - new Insertion.Before('element-insertions-main', 'some backward-compatibility testing before'); - this.assert(getInnerHTML('element-insertions-container').include('some backward-compatibility testing before')); - new Insertion.After('element-insertions-main', 'some backward-compatibility testing after'); - this.assert(getInnerHTML('element-insertions-container').include('some backward-compatibility testing after')); - new Insertion.Top('element-insertions-main', 'some backward-compatibility testing top'); - this.assert(getInnerHTML('element-insertions-main').startsWith('some backward-compatibility testing top')); - new Insertion.Bottom('element-insertions-main', 'some backward-compatibility testing bottom'); - this.assert(getInnerHTML('element-insertions-main').endsWith('some backward-compatibility testing bottom')); - }, - - testElementWrap: function() { - var element = $('wrap'), parent = document.createElement('div'); - element.wrap(); - this.assert(getInnerHTML('wrap-container').startsWith('
    \ntestVar="hello!";\n'); - this.assertEqual('hello from div!',$('testdiv').innerHTML); - this.wait(100,function(){ - this.assertEqual('hello!',testVar); - - Element.update('testdiv','another hello from div!\n - <%= script_tag('assets/prototype.js') %> - <%= script_tag('lib_assets/unittest.js') %> - <%= link_tag('lib_assets/unittest.css') %> - <%= css_fixtures %> - <%= js_fixtures %> - <%= test_file %> - - - -
    - -<%= html_fixtures %> - - - diff --git a/test/unit/tests/ajax.test.js b/test/unit/tests/ajax.test.js new file mode 100644 index 000000000..de74074dd --- /dev/null +++ b/test/unit/tests/ajax.test.js @@ -0,0 +1,545 @@ +Fixtures.Ajax = { + js: { + responseBody: '$("content").update("

    Hello world!

    ");', + 'Content-Type': ' text/javascript ' + }, + + html: { + responseBody: "Pack my box with five dozen liquor jugs! " + + "Oh, how quickly daft jumping zebras vex..." + }, + + xml: { + responseBody: 'bar', + 'Content-Type': 'application/xml' + }, + + json: { + responseBody: '{\n\r"test": 123}', + 'Content-Type': 'application/json' + }, + + jsonWithoutContentType: { + responseBody: '{"test": 123}' + }, + + invalidJson: { + responseBody: '{});window.attacked = true;({}', + 'Content-Type': 'application/json' + }, + + headerJson: { + 'X-JSON': '{"test": "hello #éà"}' + } +}; + +var responderCounter = 0; + +// lowercase comparison because of MSIE which presents HTML tags in uppercase +var sentence = ("Pack my box with five dozen liquor jugs! " + + "Oh, how quickly daft jumping zebras vex...").toLowerCase(); + +var message = 'You must be running a test server to test this feature.'; + +function assertContent(id, content, message) { + var a = content.toLowerCase(); + var b = $(id).innerHTML.strip().toLowerCase(); + message = message || 'failure'; + + assert.equal( + a, + b, + message + ': element #' + id + ' should have content: (' + a + ') but has content: (' + b + ')' + ); +} + + +var extendDefault = function(options) { + return Object.extend({ + asynchronous: false, + method: 'get' + }, options); +}; + +suite("Ajax", function () { + this.timeout(10000); + this.name = 'ajax'; + + setup(function () { + $('content', 'content2').invoke('update', ''); + }); + + teardown(function () { + // hack to cleanup responders + Ajax.Responders.responders = [Ajax.Responders.responders[0]]; + }); + + test("synchronous request", function () { + assert.equal("", $('content').innerHTML); + assert.equal(0, Ajax.activeRequestCount); + + new Ajax.Request('/fixtures/hello.js', { + asynchronous: false, + method: 'GET', + evalJS: 'force' + }); + + assert.equal(0, Ajax.activeRequestCount); + }); + + test("asynchronous request", function (done) { + assert.equal("", $("content").innerHTML); + + new Ajax.Request('/fixtures/hello.js', { + asynchronous: true, + method: 'GET', + evalJS: 'force' + }); + + setTimeout(function () { + var h2 = $('content').firstChild; + assert.equal("Hello world!", h2.innerHTML); + done(); + }, 1000); + }); + + test("activeRequestCount decrements when an exception occurs in a handler", function (done) { + + new Ajax.Request('/fixtures/hello.js', { + method: 'GET', + onComplete: function () { + assert.equal(1, Ajax.activeRequestCount); + setTimeout(function () { + assert.equal(0, Ajax.activeRequestCount); + done(); + }, 250); + throw new Error('test'); + }, + + onException: function () { + // Empty function to prevent the error from being rethrown + } + }); + + }); + + suite('Updater', function () { + + setup(function () { + $('content', 'content2').invoke('update', ''); + }); + + test('basic', function (done) { + assert.equal("", $('content').innerHTML); + + new Ajax.Updater('content', '/fixtures/content.html', { method: 'get' }); + + setTimeout(function () { + assertContent('content', sentence, 'simple updater'); + + $('content').update(''); + assert.equal("", $('content').innerHTML); + + new Ajax.Updater( + { success: 'content', failure: 'content2' }, + '/fixtures/content.html', + { method: 'get', parameters: { pet: 'monkey' } } + ); + + setTimeout(function () { + assertContent('content', sentence, 'success/failure updater'); + assertContent('content2', '', 'failure DIV should be empty'); + done(); + }, 1000); + + }, 1000); + + }); + + test('with insertion', function (done) { + $('content').update(); + new Ajax.Updater('content', '/fixtures/content.html', { + method: 'get', + insertion: Insertion.Top + }); + assertContent('content', ''); + + setTimeout(function() { + assertContent('content', sentence, 'Insertion.Top'); + $('content').update(); + + new Ajax.Updater('content', '/fixtures/content.html', { + method: 'get', + insertion: 'bottom' + }); + + setTimeout(function () { + assertContent('content', sentence, 'bottom insertion'); + $('content').update(); + + new Ajax.Updater('content', '/fixtures/content.html', { + method: 'get', + insertion: 'after' + }); + + setTimeout(function () { + assert.equal( + 'five dozen', + $('content').next().innerHTML.strip().toLowerCase(), + 'after insertion' + ); + done(); + }, 1000); + + }, 1000); + + }, 1000); + + }); + + test('with options', function () { + var options = { + method: 'get', + asynchronous: false, + evalJS: 'force', + onComplete: Prototype.emptyFunction + }; + + var request = new Ajax.Updater('content', '/fixtures/hello.js', options); + request.options.onComplete = Prototype.emptyFunction; + assert.strictEqual(Prototype.emptyFunction, options.onComplete); + }); + + }); // Updater + + test('responders', function (done) { + var r = Ajax.Responders.responders; + assert.equal(1, r.length); + + var dummyResponder = { + onComplete: Prototype.emptyFunction + }; + + Ajax.Responders.register(dummyResponder); + assert.equal(2, r.length); + + // Don't add twice. + Ajax.Responders.register(dummyResponder); + assert.equal(2, r.length, 'what'); + + Ajax.Responders.unregister(dummyResponder); + assert.equal(1, Ajax.Responders.responders.length); + + var responder = { + onCreate: function(req) { responderCounter++; }, + onLoading: function(req) { responderCounter++; }, + onComplete: function(req) { responderCounter++; } + }; + Ajax.Responders.register(responder); + + assert.equal(0, responderCounter); + assert.equal(0, Ajax.activeRequestCount); + + new Ajax.Request('/fixtures/content.html', { + method: 'get', + parameters: 'pet=monkey' + }); + + assert.equal(1, responderCounter); + assert.equal(1, Ajax.activeRequestCount); + + setTimeout(function () { + assert.equal(3, responderCounter); + assert.equal(0, Ajax.activeRequestCount); + done(); + }, 1000); + + }); + + test('eval response should be called before onComplete', function () { + assert.equal('', $('content').innerHTML); + assert.equal(0, Ajax.activeRequestCount); + + new Ajax.Request('/fixtures/hello.js', extendDefault({ + onComplete: function(response) { + assert.notEqual('', $('content').innerHTML); + } + })); + assert.equal(0, Ajax.activeRequestCount); + + var h2 = $('content').firstChild; + assert.equal('Hello world!', h2.innerHTML); + }); + + test('Content-Type set for obscure verbs', function () { + new Ajax.Request('/inspect', extendDefault({ + method: 'put', + contentType: 'application/bogus', + onComplete: function (response) { + assert.equal( + 'application/bogus; charset=UTF-8', + response.responseJSON.headers['content-type'] + ); + } + })); + }); + + test('verbs with bodies', function () { + var verbs = $w('post put patch'); + verbs.each(function (verb) { + new Ajax.Request('/inspect', extendDefault({ + method: verb, + body: 'foo=foo&bar=bar', + onSuccess: function (response) { + var body = response.responseJSON.body; + assert.equal('foo=foo&bar=bar', body, verb + ' should send body'); + }, + onFailure: function () { + assert(false, verb + ' should send body'); + } + })); + }); + }); + + test('verbs without bodies', function () { + var verbs = $w('get head options delete'); + + verbs.each(function (verb) { + new Ajax.Request('/inspect', extendDefault({ + method: verb, + onSuccess: function () { + assert(true, verb + ' method should work'); + }, + onFailure: function () { + assert(false, verb + ' method should work'); + } + })); + }); + }); + + test('onCreate callback', function () { + new Ajax.Request('/fixtures/content.html', extendDefault({ + onCreate: function (transport) { + assert.equal(0, transport.readyState); + }, + onComplete: function (transport) { + assert.notEqual(0, transport.readyState); + } + })); + }); + + test('evalJS', function () { + $('content').update(); + new Ajax.Request('/response', extendDefault({ + parameters: Fixtures.Ajax.js, + onComplete: function (transport) { + var h2 = $('content').firstChild; + assert.equal('Hello world!', h2.innerHTML); + } + })); + + $('content').update(); + new Ajax.Request('/response', extendDefault({ + evalJS: false, + parameters: Fixtures.Ajax.js, + onComplete: function () { + assert.equal('', $('content').innerHTML); + } + })); + + $('content').update(); + new Ajax.Request("/fixtures/hello.js", extendDefault({ + evalJS: 'force', + onComplete: function(transport) { + var h2 = $('content').firstChild; + assert.equal('Hello world!', h2.innerHTML); + } + })); + + }); + + test('callbacks', function () { + var options = extendDefault({ + onCreate: function (transport) { + assert.isInstanceOf(transport, Ajax.Response); + } + }); + + Ajax.Request.Events.each(function (state) { + options['on' + state] = options.onCreate; + }); + + new Ajax.Request('/fixtures/content.html', options); + }); + + test('response text', function () { + new Ajax.Request('/fixtures/empty.html', extendDefault({ + onComplete: function (transport) { + assert.equal('', transport.responseText); + } + })); + + new Ajax.Request('/fixtures/content.html', extendDefault({ + onComplete: function (transport) { + assert.equal(sentence, transport.responseText.toLowerCase()); + } + })); + }); + + test('responseXML', function () { + new Ajax.Request('/response', extendDefault({ + parameters: Fixtures.Ajax.xml, + onComplete: function (transport) { + assert.equal( + 'foo', + transport.responseXML.getElementsByTagName('name')[0].getAttribute('attr') + ); + } + })); + }); + + test('responseJSON', function () { + new Ajax.Request('/response', extendDefault({ + parameters: Fixtures.Ajax.json, + onComplete: function (transport) { + assert.equal(123, transport.responseJSON.test); + } + })); + + new Ajax.Request('/response', extendDefault({ + parameters: { + 'Content-Length': 0, + 'Content-Type': 'application/json' + }, + onComplete: function (transport) { + assert.isNull(transport.responseJSON); + } + })); + + new Ajax.Request('/response', extendDefault({ + evalJSON: false, + parameters: Fixtures.Ajax.json, + onComplete: function (transport) { + assert.isNull(transport.responseJSON); + } + })); + + new Ajax.Request('/response', extendDefault({ + sanitizeJSON: true, + parameters: Fixtures.Ajax.invalidJson, + onException: function (request, error) { + assert.equal('SyntaxError', error.name); + } + })); + + new Ajax.Request('/fixtures/data.json', extendDefault({ + evalJSON: 'force', + onComplete: function (transport) { + assert.equal(123, transport.responseJSON.test); + } + })); + }); + + test('headerJSON', function () { + new Ajax.Request('/response', extendDefault({ + parameters: Fixtures.Ajax.headerJson, + onComplete: function (transport, json) { + assert.equal('hello #éà', transport.headerJSON.test); + assert.equal('hello #éà', json.test); + } + })); + + new Ajax.Request('/response', extendDefault({ + onComplete: function (transport, json) { + assert.isNull(transport.headerJSON); + assert.isNull(json); + } + })); + }); + + test('getHeader', function () { + new Ajax.Request('/response', extendDefault({ + parameters: { 'X-TEST': 'some value' }, + onComplete: function (transport) { + assert.equal('some value', transport.getHeader('X-Test')); + assert.isNull(transport.getHeader('X-Non-Existent')); + } + })); + }); + + test('parameters can be a hash', function () { + new Ajax.Request('/response', extendDefault({ + parameters: $H({ one: "two", three: "four" }), + onComplete: function (transport) { + assert.equal('two', transport.getHeader('one')); + assert.equal('four', transport.getHeader('three')); + assert.isNull(transport.getHeader('toObject')); + } + })); + }); + + test('parameters string order is preserved', function () { + new Ajax.Request('/inspect', extendDefault({ + parameters: "cool=1&bad=2&cool=3&bad=4", + method: 'post', + onComplete: function (transport) { + var bodyWithoutWart = + transport.responseJSON.body.match(/((?:(?!&_=$).)*)/)[1]; + assert.equal('cool=1&bad=2&cool=3&bad=4', bodyWithoutWart); + } + })); + }); + + test('isSameOrigin', function () { + var isSameOrigin = Ajax.Request.prototype.isSameOrigin; + + assert(isSameOrigin.call({ url: '/foo/bar.html' }), + '/foo/bar.html should be same-origin'); + assert(isSameOrigin.call({ url: window.location.toString() }), + 'current window location should be same-origin'); + assert(!isSameOrigin.call({ url: 'http://example.com' }), + 'example.com should not be same-origin'); + + Ajax.Request.prototype.isSameOrigin = function () { + return false; + }; + + $('content').update('same origin policy'); + + new Ajax.Request('/response', extendDefault({ + parameters: Fixtures.Ajax.js, + onComplete: function (transport) { + assert.equal('same origin policy', $('content').innerHTML); + } + })); + + new Ajax.Request('/response', extendDefault({ + parameters: Fixtures.Ajax.invalidJson, + onException: function (request, error) { + assert.equal('SyntaxError', error.name); + } + })); + + new Ajax.Request('/response', extendDefault({ + parameters: { 'X-JSON': '{});window.attacked = true;({}' }, + onException: function (request, error) { + assert.equal('SyntaxError', error.name); + } + })); + + Ajax.Request.prototype.isSameOrigin = isSameOrigin; + }); + + test('can omit content-type', function () { + new Ajax.Request('/inspect', extendDefault({ + method: 'post', + contentType: false, + onSuccess: function (response) { + // If we omit Content-Type, the browser will provide its own. + var contentType = response.responseJSON.headers['content-type']; + assert(contentType.indexOf('text/plain') > -1); + } + })); + }); +}); + diff --git a/test/unit/tests/array.test.js b/test/unit/tests/array.test.js new file mode 100644 index 000000000..ea6d1acfc --- /dev/null +++ b/test/unit/tests/array.test.js @@ -0,0 +1,355 @@ +var globalArgsTest = 'nothing to see here'; + +suite('Array', function () { + this.name = 'array'; + + test('$A', function () { + assert.enumEqual([], $A({})); + }); + + test('$A (on arguments)', function () { + function toArrayOnArguments(){ + globalArgsTest = $A(arguments); + } + toArrayOnArguments(); + assert.enumEqual([], globalArgsTest); + toArrayOnArguments('foo'); + assert.enumEqual(['foo'], globalArgsTest); + toArrayOnArguments('foo','bar'); + assert.enumEqual(['foo','bar'], globalArgsTest); + }); + + test('$A (on NodeList)', function () { + // direct HTML + assert.equal(3, $A($('test_node').childNodes).length); + + // DOM + var element = document.createElement('div'); + element.appendChild(document.createTextNode('22')); + (2).times(function(){ element.appendChild(document.createElement('span')); }); + assert.equal(3, $A(element.childNodes).length); + + // HTML String + element = document.createElement('div'); + $(element).update('22 2; + })); + assert(![1,2,3,4,5].any(function(value) { + return value > 5; + })); + + var x = [1,2,3], traversed = []; + delete x[1]; + x.any(function(val) { traversed.push(val); }); + assert.enumEqual([1, 3], traversed); + assert.strictEqual(2, traversed.length); + }); + + test('#any (used as generic)', function () { + assert(Array.prototype.any.call({ 0:false, 1:true, length:2 })); + assert(!Array.prototype.any.call({ 0:false, 1:false, length:2 })); + }); + + test('#all', function () { + assert([].all()); + + assert([true, true, true].all()); + assert(![true, false, false].all()); + assert(![false, false, false].all()); + + assert([1,2,3,4,5].all(function(value) { + return value > 0; + })); + assert(![1,2,3,4,5].all(function(value) { + return value > 1; + })); + + var x = [1,2,3], traversed = []; + delete x[1]; + x.all(function(val) { traversed.push(val); return true; }); + assert.enumEqual([1, 3], traversed); + assert.strictEqual(2, traversed.length); + }); + + test('#all (used as generic)', function () { + assert(Array.prototype.all.call({ 0:true, 1:true, length:2 })); + assert(!Array.prototype.all.call({ 0:false, 1:true, length:2 })); + }); + + test('#entries (should be either nonexistent or native)', function () { + assert(Array.prototype.entries !== Enumerable.entries); + }); + +}); diff --git a/test/unit/tests/base.test.js b/test/unit/tests/base.test.js new file mode 100644 index 000000000..6c6925164 --- /dev/null +++ b/test/unit/tests/base.test.js @@ -0,0 +1,57 @@ + +suite('Base', function () { + + this.name = 'base'; + + test('Browser detection', function () { + + var results = $H(Prototype.Browser).map(function (engine) { + return engine; + }).partition(function (engine) { + return engine[1] === true; + }); + var trues = results[0], falses = results[1]; + + info('User agent string is: ' + navigator.userAgent); + + // It's OK for there to be two true values if we're on MobileSafari, + // since it's also a WebKit browser. + if (Prototype.Browser.MobileSafari) { + assert(trues.size() === 2, 'MobileSafari should also identify as WebKit.'); + } else { + assert(trues.size() === 0 || trues.size() === 1, + 'There should be only one or no browser detected.'); + } + + // we should have definite trues or falses here + trues.each(function(result) { + assert(result[1] === true); + }, this); + falses.each(function(result) { + assert(result[1] === false); + }, this); + + var ua = navigator.userAgent; + + if (ua.indexOf('AppleWebKit/') > -1) { + info('Running on WebKit'); + assert(Prototype.Browser.WebKit); + } + + if (Object.prototype.toString.call(window.opera) === '[object Opera]') { + info('Running on Opera'); + assert(Prototype.Browser.Opera); + } + + if (ua.indexOf('MSIE') > -1) { + info('Running on IE'); + assert(Prototype.Browser.IE); + } + + if (ua.indexOf('Gecko') > -1 && ua.indexOf('KHTML') === -1) { + info('Running on Gecko'); + assert(Prototype.Browser.Gecko); + } + }); + +}); \ No newline at end of file diff --git a/test/unit/tests/class.test.js b/test/unit/tests/class.test.js new file mode 100644 index 000000000..bb41a468a --- /dev/null +++ b/test/unit/tests/class.test.js @@ -0,0 +1,223 @@ +// base class +var Animal = Class.create({ + initialize: function(name) { + this.name = name; + }, + name: "", + eat: function() { + return this.say("Yum!"); + }, + say: function(message) { + return this.name + ": " + message; + } +}); + +// subclass that augments a method +var Cat = Class.create(Animal, { + eat: function($super, food) { + if (food instanceof Mouse) return $super(); + else return this.say("Yuk! I only eat mice."); + } +}); + +// empty subclass +var Mouse = Class.create(Animal, {}); + +//mixins +var Sellable = { + getValue: function(pricePerKilo) { + return this.weight * pricePerKilo; + }, + + inspect: function() { + return '#'.interpolate(this); + } +}; + +var Reproduceable = { + reproduce: function(partner) { + if (partner.constructor != this.constructor || partner.sex == this.sex) + return null; + var weight = this.weight / 10, sex = Math.random(1).round() ? 'male' : 'female'; + return new this.constructor('baby', weight, sex); + } +}; + +// base class with mixin +var Plant = Class.create(Sellable, { + initialize: function(name, weight) { + this.name = name; + this.weight = weight; + }, + + inspect: function() { + return '#'.interpolate(this); + } +}); + +// subclass with mixin +var Dog = Class.create(Animal, Reproduceable, { + initialize: function($super, name, weight, sex) { + this.weight = weight; + this.sex = sex; + $super(name); + } +}); + +// subclass with mixins +var Ox = Class.create(Animal, Sellable, Reproduceable, { + initialize: function($super, name, weight, sex) { + this.weight = weight; + this.sex = sex; + $super(name); + }, + + eat: function(food) { + if (food instanceof Plant) + this.weight += food.weight; + }, + + inspect: function() { + return '#'.interpolate(this); + } +}); + + + + +suite("Class", function () { + this.name = 'class'; + + test('create', function () { + assert(Object.isFunction(Animal), 'Animal is not a constructor'); + }); + + test('instantiation', function () { + var pet = new Animal('Nibbles'); + assert.equal('Nibbles', pet.name, 'property not initialized'); + assert.equal('Nibbles: Hi!', pet.say('Hi!')); + assert.equal(Animal, pet.constructor, 'bad constructor reference'); + assert.isUndefined(pet.superclass); + + var Empty = Class.create(); + assert.equal('object', typeof new Empty); + }); + + test('inheritance', function () { + var tom = new Cat('Tom'); + assert.equal(Cat, tom.constructor, 'bad constructor reference'); + assert.equal(Animal, tom.constructor.superclass, 'bad superclass reference'); + assert.equal('Tom', tom.name); + assert.equal('Tom: meow', tom.say('meow')); + assert.equal('Tom: Yuk! I only eat mice.', tom.eat(new Animal)); + }); + + test('superclass method call', function () { + var tom = new Cat('Tom'); + assert.equal('Tom: Yum!', tom.eat(new Mouse)); + + var Dodo = Class.create(Animal, { + initialize: function ($super, name) { + $super(name); + this.extinct = true; + }, + + say: function ($super, message) { + return $super(message) + " honk honk"; + } + }); + + var gonzo = new Dodo('Gonzo'); + assert.equal('Gonzo', gonzo.name); + assert(gonzo.extinct, 'Dodo birds should be extinct'); + assert.equal('Gonzo: hello honk honk', gonzo.say('hello')); + }); + + test('addMethods', function () { + var tom = new Cat('Tom'); + var jerry = new Mouse('Jerry'); + + Animal.addMethods({ + sleep: function () { + return this.say('ZZZ'); + } + }); + + Mouse.addMethods({ + sleep: function ($super) { + return $super() + " ... no, can't sleep! Gotta steal cheese!"; + }, + + escape: function (cat) { + return this.say('(from a mousehole) Take that, ' + cat.name + '!'); + } + }); + + assert.equal('Tom: ZZZ', tom.sleep(), + 'added instance method not available to subclass'); + assert.equal("Jerry: ZZZ ... no, can't sleep! Gotta steal cheese!", + jerry.sleep()); + assert.equal("Jerry: (from a mousehole) Take that, Tom!", + jerry.escape(tom)); + + // Ensure that a method has not propagated _up_ the prototype chain. + assert.isUndefined(tom.escape); + assert.isUndefined(new Animal().escape); + + Animal.addMethods({ + sleep: function () { + return this.say('zZzZ'); + } + }); + + assert.equal("Jerry: zZzZ ... no, can't sleep! Gotta steal cheese!", + jerry.sleep()); + + }); + + test('base class with mixin', function () { + var grass = new Plant('grass', 3); + assert.respondsTo('getValue', grass); + assert.equal('#', grass.inspect()); + }); + + test('subclass with mixin', function () { + var snoopy = new Dog('Snoopy', 12, 'male'); + assert.respondsTo('reproduce', snoopy); + }); + + test('subclass with mixins', function () { + var cow = new Ox('cow', 400, 'female'); + assert.equal('#', cow.inspect()); + assert.respondsTo('reproduce', cow); + assert.respondsTo('getValue', cow); + }); + + test('class with toString and valueOf methods', function () { + var Foo = Class.create({ + toString: function() { return "toString"; }, + valueOf: function() { return "valueOf"; } + }); + + var Bar = Class.create(Foo, { + valueOf: function() { return "myValueOf"; } + }); + + var Parent = Class.create({ + m1: function(){ return 'm1'; }, + m2: function(){ return 'm2'; } + }); + var Child = Class.create(Parent, { + m1: function($super) { return 'm1 child'; }, + m2: function($super) { return 'm2 child'; } + }); + + assert(new Child().m1.toString().indexOf('m1 child') > -1); + + assert.equal("toString", new Foo().toString()); + assert.equal("valueOf", new Foo().valueOf() ); + assert.equal("toString", new Bar().toString()); + assert.equal("myValueOf", new Bar().valueOf() ); + }); + +}); \ No newline at end of file diff --git a/test/unit/tests/date.test.js b/test/unit/tests/date.test.js new file mode 100644 index 000000000..8a13c3c80 --- /dev/null +++ b/test/unit/tests/date.test.js @@ -0,0 +1,18 @@ +suite('Date', function () { + this.name = 'date'; + + test('#toJSON', function () { + assert.match( + new Date(Date.UTC(1970, 0, 1)).toJSON(), + /^1970-01-01T00:00:00(\.000)?Z$/ + ); + }); + + test('#toISOString', function () { + assert.match( + new Date(Date.UTC(1970, 0, 1)).toISOString(), + /^1970-01-01T00:00:00(\.000)?Z$/ + ); + }); + +}); \ No newline at end of file diff --git a/test/unit/tests/dom.test.js b/test/unit/tests/dom.test.js new file mode 100644 index 000000000..1ef310a33 --- /dev/null +++ b/test/unit/tests/dom.test.js @@ -0,0 +1,1543 @@ +var testVar = 'to be updated', testVar2 = ''; + +Element.addMethods({ + hashBrowns: function(element) { return 'hash browns'; } +}); + +Element.addMethods("LI", { + pancakes: function(element) { return "pancakes"; } +}); + +Element.addMethods("DIV", { + waffles: function(element) { return "waffles"; } +}); + +Element.addMethods($w("li div"), { + orangeJuice: function(element) { return "orange juice"; } +}); + + +function getInnerHTML (id) { + return $(id).innerHTML.toString().toLowerCase().gsub(/[\r\n\t]/, ''); +} + +function createParagraph (text) { + var p = document.createElement('p'); + p.appendChild(document.createTextNode(text)); + return p; +} + +var RESIZE_DISABLED = false; + +function simulateClick(node) { + var oEvent; + if (document.createEvent) { + oEvent = document.createEvent('MouseEvents'); + oEvent.initMouseEvent('click', true, true, document.defaultView, + 0, 0, 0, 0, 0, false, false, false, false, 0, node); + node.dispatchEvent(oEvent); + } else { + node.click(); + } +} + +var documentViewportProperties = null; + + +suite('DOM', function () { + this.name = 'dom'; + + setup(function () { + + if (documentViewportProperties) return; + + // Based on properties check from http://www.quirksmode.org/viewport/compatibility.html. + + documentViewportProperties = { + properties: [ + 'self.pageXOffset', 'self.pageYOffset', + 'self.screenX', 'self.screenY', + 'self.innerHeight', 'self.innerWidth', + 'self.outerHeight', 'self.outerWidth', + 'self.screen.height', 'self.screen.width', + 'self.screen.availHeight', 'self.screen.availWidth', + 'self.screen.availTop', 'self.screen.availLeft', + 'self.screen.Top', 'self.screen.Left', + 'self.screenTop', 'self.screenLeft', + 'document.body.clientHeight', 'document.body.clientWidth', + 'document.body.scrollHeight', 'document.body.scrollWidth', + 'document.body.scrollLeft', 'document.body.scrollTop', + 'document.body.offsetHeight', 'document.body.offsetWidth', + 'document.body.offsetTop', 'document.body.offsetLeft' + ].inject([], function (properties, prop) { + if (!self.screen && prop.include('self.screen')) return; + if (!document.body && prop.include('document.body')) return; + + properties.push(prop); + + if (prop.include('body') && document.documentElement) { + properties.push(prop.sub('.body', '.documentElement')); + } + + return properties; + }), + + inspect: function () { + var props = []; + this.properties.each(function (prop) { + if (eval(prop)) props[prop] = eval(prop); + }, this); + return props; + } + }; + }); + + + + test('$', function () { + assert.isUndefined($(), '$() should be undefined'); + + assert.isNull(document.getElementById('noWayThisIDExists'), + 'nonexistent ID should return null from getElementById'); + + assert.isNull($('noWayThisIDExists'), + 'nonexistent ID should return null from $'); + + assert.strictEqual(document.getElementById('testdiv'), $('testdiv'), + 'getElementById and $ should return the same element'); + + assert.deepEqual( + [ $('testdiv'), $('container') ], + $('testdiv', 'container') + ); + + assert.deepEqual( + [ $('testdiv'), null, $('container') ], + $('testdiv', 'noWayThisIDExists', 'container') + ); + + var elt = $('testdiv'); + + assert.strictEqual(elt, $(elt)); + assert.respondsTo('hide', elt); + assert.respondsTo('childOf', elt); + }); + + + test('.insert (with HTML)', function () { + + Element.insert('insertions-main', { + before:'

    before text

    more testing

    ' + }); + assert(getInnerHTML('insertions-container').startsWith('

    before text

    more testing

    ')); + + Element.insert('insertions-main', { + after:'

    after text

    more testing

    ' + }); + assert(getInnerHTML('insertions-container').endsWith('

    after text

    more testing

    ')); + + Element.insert('insertions-main', { + top:'

    top text.

    more testing

    ' + }); + assert(getInnerHTML('insertions-main').startsWith('

    top text.

    more testing

    ')); + + Element.insert('insertions-main', { + bottom:'

    bottom text.

    more testing

    ' + }); + assert(getInnerHTML('insertions-main').endsWith('

    bottom text.

    more testing

    ')); + + }); + + + test('.insert (with DOM node)', function () { + Element.insert('insertions-node-main', { + before: createParagraph('node before') + }); + assert(getInnerHTML('insertions-node-container').startsWith('

    node before

    ')); + + Element.insert('insertions-node-main', { + after: createParagraph('node after') + }); + assert(getInnerHTML('insertions-node-container').endsWith('

    node after

    ')); + + Element.insert('insertions-node-main', { + top: createParagraph('node top') + }); + assert(getInnerHTML('insertions-node-main').startsWith('

    node top

    ')); + + Element.insert('insertions-node-main', { + bottom: createParagraph('node bottom')} + ); + assert(getInnerHTML('insertions-node-main').endsWith('

    node bottom

    ')); + + assert.equal( + $('insertions-node-main'), + $('insertions-node-main').insert(document.createElement('p')), + 'insert should return the original node' + ); + }); + + + test('.insert (with toElement method)', function () { + Element.insert('insertions-node-main', { + toElement: createParagraph.curry('toElement') + }); + assert(getInnerHTML('insertions-node-main').endsWith('

    toelement

    ')); + + Element.insert('insertions-node-main', { + bottom: { toElement: createParagraph.curry('bottom toElement') } + }); + assert(getInnerHTML('insertions-node-main').endsWith('

    bottom toelement

    ')); + }); + + + test('.insert (with toHTML method)', function () { + Element.insert('insertions-node-main', { + toHTML: function() { return '

    toHTML

    '; } + }); + assert(getInnerHTML('insertions-node-main').endsWith('

    tohtml

    ')); + + Element.insert('insertions-node-main', { + bottom: { + toHTML: function() { return '

    bottom toHTML

    '; } + } + }); + assert(getInnerHTML('insertions-node-main').endsWith('

    bottom tohtml

    ')); + }); + + test('.insert (with non-string)', function () { + Element.insert('insertions-main', { bottom: 3 }); + assert(getInnerHTML('insertions-main').endsWith('3')); + }); + + test('.insert (in tables)', function () { + Element.insert('second_row', { + after:'Third Row' + }); + assert($('second_row').parentNode == $('table'), + 'table rows should be inserted correctly'); + + $('a_cell').insert({ top: 'hello world' }); + assert($('a_cell').innerHTML.startsWith('hello world'), + 'content should be inserted into table cells correctly'); + + $('a_cell').insert({ after: 'hi planet' }); + assert.equal('hi planet', $('a_cell').next().innerHTML, + 'table cells should be inserted after existing table cells correctly'); + + $('table_for_insertions').insert('a cell!'); + assert($('table_for_insertions').innerHTML.gsub('\r\n', '').toLowerCase().include('a cell!'), + 'complex content should be inserted into a table correctly'); + + $('row_1').insert({ after:'last' }); + assert.equal('last', $A($('table_for_row_insertions').getElementsByTagName('tr')).last().lastChild.innerHTML, + 'complex content should be inserted after a table row correctly'); + }); + + test('.insert (in select)', function () { + var selectTop = $('select_for_insert_top'); + var selectBottom = $('select_for_insert_bottom'); + + selectBottom.insert(''); + assert.equal('option 45', selectBottom.getValue()); + selectTop.insert({top:''}); + assert.equal(4, selectTop.options.length); + }); + + test('#insert', function () { + $('element-insertions-main').insert({before:'some text before'}); + assert(getInnerHTML('element-insertions-container').startsWith('some text before'), 'some text before'); + $('element-insertions-main').insert({after:'some text after'}); + assert(getInnerHTML('element-insertions-container').endsWith('some text after'), 'some text after'); + $('element-insertions-main').insert({top:'some text top'}); + assert(getInnerHTML('element-insertions-main').startsWith('some text top'), 'some text top'); + $('element-insertions-main').insert({bottom:'some text bottom'}); + assert(getInnerHTML('element-insertions-main').endsWith('some text bottom'), 'some text bottom'); + + $('element-insertions-main').insert('some more text at the bottom'); + assert(getInnerHTML('element-insertions-main').endsWith('some more text at the bottom'), + 'some more text at the bottom'); + + $('element-insertions-main').insert({TOP:'some text uppercase top'}); + assert(getInnerHTML('element-insertions-main').startsWith('some text uppercase top'), 'some text uppercase top'); + + $('element-insertions-multiple-main').insert({ + top:'1', bottom:2, before: new Element('p').update('3'), after:'4' + }); + assert(getInnerHTML('element-insertions-multiple-main').startsWith('1'), '1'); + assert(getInnerHTML('element-insertions-multiple-main').endsWith('2'), '2'); + assert(getInnerHTML('element-insertions-multiple-container').startsWith( + '

    3

    '), '

    3

    '); + assert(getInnerHTML('element-insertions-multiple-container').endsWith('4'), '4'); + + $('element-insertions-main').update('test'); + $('element-insertions-main').insert(null); + $('element-insertions-main').insert({bottom:null}); + assert.equal('test', getInnerHTML('element-insertions-main'), 'should insert nothing when called with null'); + $('element-insertions-main').insert(1337); + assert.equal('test1337', getInnerHTML('element-insertions-main'), 'should coerce to string when called with number'); + }); + + + test('#insert (with new Element)', function () { + var container = new Element('div'), element = new Element('div'); + container.insert(element); + + element.insert({ before: '

    a paragraph

    ' }); + assert.equal('

    a paragraph

    ', getInnerHTML(container)); + element.insert({ after: 'some text' }); + assert.equal('

    a paragraph

    some text', getInnerHTML(container)); + + element.insert({ top: '

    a paragraph

    ' }); + assert.equal('

    a paragraph

    ', getInnerHTML(element)); + element.insert('some text'); + assert.equal('

    a paragraph

    some text', getInnerHTML(element)); + + }); + + + test('Insertion (backwards-compatibility)', function () { + new Insertion.Before('element-insertions-main', 'some backward-compatibility testing before'); + assert(getInnerHTML('element-insertions-container').include('some backward-compatibility testing before')); + new Insertion.After('element-insertions-main', 'some backward-compatibility testing after'); + assert(getInnerHTML('element-insertions-container').include('some backward-compatibility testing after')); + new Insertion.Top('element-insertions-main', 'some backward-compatibility testing top'); + assert(getInnerHTML('element-insertions-main').startsWith('some backward-compatibility testing top')); + new Insertion.Bottom('element-insertions-main', 'some backward-compatibility testing bottom'); + assert(getInnerHTML('element-insertions-main').endsWith('some backward-compatibility testing bottom')); + }); + + test('#wrap', function () { + var element = $('wrap'), parent = document.createElement('div'); + element.wrap(); + assert(getInnerHTML('wrap-container').startsWith('
    \ntestVar="hello!";\n'); + assert.equal('hello from div!', $('testdiv').innerHTML); + + wait(100, done, function () { + assert.equal('hello!', testVar); + + Element.update('testdiv','another hello from div!\n" + + str.evalScripts.bind(str).defer(); + + wait(50, done, function() { + assert(window.deferBoundProperlyOnString); + done(); + }); + }); + }); + }); + }); + + test('#methodize', function () { + var Foo = { bar: function(baz) { return baz; } }; + var baz = { quux: Foo.bar.methodize() }; + + assert.equal(Foo.bar.methodize(), baz.quux); + assert.equal(baz, Foo.bar(baz)); + assert.equal(baz, baz.quux()); + }); + + test('#bindAsEventListener', function () { + for (var i = 0; i < 10; ++i){ + var div = document.createElement('div'); + div.setAttribute('id','test-'+i); + document.body.appendChild(div); + var tobj = new TestObj(); + var eventTest = { test: true }; + var call = tobj.assertingEventHandler.bindAsEventListener(tobj, + assert.equal.bind(assert, eventTest), + assert.equal.bind(assert, arg1), + assert.equal.bind(assert, arg2), + assert.equal.bind(assert, arg3), arg1, arg2, arg3 ); + call(eventTest); + } + }); + +}); diff --git a/test/unit/tests/hash.test.js b/test/unit/tests/hash.test.js new file mode 100644 index 000000000..437baedd6 --- /dev/null +++ b/test/unit/tests/hash.test.js @@ -0,0 +1,240 @@ +Fixtures.Hash = { + one: { a: 'A#' }, + + many: { + a: 'A', + b: 'B', + c: 'C', + d: 'D#' + }, + + functions: { + quad: function(n) { return n*n; }, + plus: function(n) { return n+n; } + }, + + multiple: { color: $w('r g b') }, + multiple_nil: { color: ['r', null, 'g', undefined, 0] }, + multiple_all_nil: { color: [null, undefined] }, + multiple_empty: { color: [] }, + multiple_special: { 'stuff[]': $w('$ a ;') }, + + value_undefined: { a:"b", c:undefined }, + value_null: { a:"b", c:null }, + value_zero: { a:"b", c:0 } +}; + + +/// + + +suite('Hash', function () { + this.name = 'hash'; + + test('#set', function () { + var h = $H({a: 'A'}); + + assert.equal('B', h.set('b', 'B')); + assert.hashEqual({a: 'A', b: 'B'}, h); + + assert.isUndefined(h.set('c')); + assert.hashEqual({a: 'A', b: 'B', c: undefined}, h); + }); + + test('#get', function () { + var h = $H({a: 'A'}); + assert.equal('A', h.get('a')); + assert.isUndefined(h.a); + assert.isUndefined($H({}).get('a')); + + assert.isUndefined($H({}).get('toString')); + assert.isUndefined($H({}).get('constructor')); + }); + + test('#unset', function () { + var hash = $H(Fixtures.Hash.many); + assert.equal('B', hash.unset('b')); + assert.hashEqual({a:'A', c: 'C', d:'D#'}, hash); + assert.isUndefined(hash.unset('z')); + assert.hashEqual({a:'A', c: 'C', d:'D#'}, hash); + // not equivalent to Hash#remove + assert.equal('A', hash.unset('a', 'c')); + assert.hashEqual({c: 'C', d:'D#'}, hash); + }); + + test('#toObject', function () { + var hash = $H(Fixtures.Hash.many), object = hash.toObject(); + assert.isInstanceOf(object, Object); + assert.hashEqual(Fixtures.Hash.many, object); + assert.notStrictEqual(Fixtures.Hash.many, object); + hash.set('foo', 'bar'); + assert.hashNotEqual(object, hash.toObject()); + }); + + test('new Hash', function () { + var object = Object.clone(Fixtures.Hash.one); + var h = new Hash(object), h2 = $H(object); + assert.isInstanceOf(h, Hash); + assert.isInstanceOf(h2, Hash); + + assert.hashEqual({}, new Hash()); + assert.hashEqual(object, h); + assert.hashEqual(object, h2); + + h.set('foo', 'bar'); + assert.hashNotEqual(object, h); + + var clone = $H(h); + assert.isInstanceOf(clone, Hash); + assert.hashEqual(h, clone); + h.set('foo', 'foo'); + assert.hashNotEqual(h, clone); + assert.strictEqual($H, Hash.from); + }); + + test('#keys', function () { + assert.enumEqual([], $H({}).keys()); + assert.enumEqual(['a'], $H(Fixtures.Hash.one).keys()); + assert.enumEqual($w('a b c d'), $H(Fixtures.Hash.many).keys().sort()); + assert.enumEqual($w('plus quad'), $H(Fixtures.Hash.functions).keys().sort()); + }); + + test('#values', function () { + assert.enumEqual([], $H({}).values()); + assert.enumEqual(['A#'], $H(Fixtures.Hash.one).values()); + assert.enumEqual($w('A B C D#'), $H(Fixtures.Hash.many).values().sort()); + assert.enumEqual($w('function function'), + $H(Fixtures.Hash.functions).values().map(function(i){ return typeof i; })); + assert.equal(9, $H(Fixtures.Hash.functions).get('quad')(3)); + assert.equal(6, $H(Fixtures.Hash.functions).get('plus')(3)); + }); + + test('#index', function () { + assert.isUndefined($H().index('foo')); + + assert('a', $H(Fixtures.Hash.one).index('A#')); + assert('a', $H(Fixtures.Hash.many).index('A')); + assert.isUndefined($H(Fixtures.Hash.many).index('Z')); + + var hash = $H({a:1,b:'2',c:1}); + assert(['a','c'].include(hash.index(1))); + assert.isUndefined(hash.index('1')); + }); + + test('#merge', function () { + var h = $H(Fixtures.Hash.many); + assert.notStrictEqual(h, h.merge()); + assert.notStrictEqual(h, h.merge({})); + assert.isInstanceOf(h.merge(), Hash); + assert.isInstanceOf(h.merge({}), Hash); + assert.hashEqual(h, h.merge()); + assert.hashEqual(h, h.merge({})); + assert.hashEqual(h, h.merge($H())); + assert.hashEqual({a:'A', b:'B', c:'C', d:'D#', aaa:'AAA' }, h.merge({aaa: 'AAA'})); + assert.hashEqual({a:'A#', b:'B', c:'C', d:'D#' }, h.merge(Fixtures.Hash.one)); + }); + + test('#update', function () { + var h = $H(Fixtures.Hash.many); + assert.strictEqual(h, h.update()); + assert.strictEqual(h, h.update({})); + assert.hashEqual(h, h.update()); + assert.hashEqual(h, h.update({})); + assert.hashEqual(h, h.update($H())); + assert.hashEqual({a:'A', b:'B', c:'C', d:'D#', aaa:'AAA' }, h.update({aaa: 'AAA'})); + assert.hashEqual({a:'A#', b:'B', c:'C', d:'D#', aaa:'AAA' }, h.update(Fixtures.Hash.one)); + }); + + test('#toQueryString', function () { + assert.equal('', $H({}).toQueryString()); + assert.equal('a%23=A', $H({'a#': 'A'}).toQueryString()); + assert.equal('a=A%23', $H(Fixtures.Hash.one).toQueryString()); + assert.equal('a=A&b=B&c=C&d=D%23', $H(Fixtures.Hash.many).toQueryString()); + assert.equal("a=b&c", $H(Fixtures.Hash.value_undefined).toQueryString()); + assert.equal("a=b&c", $H("a=b&c".toQueryParams()).toQueryString()); + assert.equal("a=b+d&c", $H("a=b+d&c".toQueryParams()).toQueryString()); + assert.equal("a=b&c=", $H(Fixtures.Hash.value_null).toQueryString()); + assert.equal("a=b&c=0", $H(Fixtures.Hash.value_zero).toQueryString()); + assert.equal("color=r&color=g&color=b", $H(Fixtures.Hash.multiple).toQueryString()); + assert.equal("color=r&color=&color=g&color&color=0", $H(Fixtures.Hash.multiple_nil).toQueryString()); + assert.equal("color=&color", $H(Fixtures.Hash.multiple_all_nil).toQueryString()); + assert.equal("", $H(Fixtures.Hash.multiple_empty).toQueryString()); + assert.equal("", $H({foo: {}, bar: {}}).toQueryString()); + assert.equal("stuff%5B%5D=%24&stuff%5B%5D=a&stuff%5B%5D=%3B", $H(Fixtures.Hash.multiple_special).toQueryString()); + assert.hashEqual(Fixtures.Hash.multiple_special, $H(Fixtures.Hash.multiple_special).toQueryString().toQueryParams()); + assert.strictEqual(Object.toQueryString, Hash.toQueryString); + + // Serializing newlines and spaces is weird. See: + // http://www.w3.org/TR/1999/REC-html401-19991224/interact/forms.html#h-17.13.4.1 + var complex = "an arbitrary line\n\'something in single quotes followed by a newline\'\r\n" + + "and more text eventually"; + var queryString = $H({ val: complex }).toQueryString(); + var expected = "val=an+arbitrary+line%0D%0A'something+in+single+quotes+followed+by+a+" + + "newline'%0D%0Aand+more+text+eventually"; + assert.equal(expected, queryString, "newlines and spaces should be properly encoded"); + }); + + test('#inspect', function () { + assert.equal('#', $H({}).inspect()); + assert.equal("#", $H(Fixtures.Hash.one).inspect()); + assert.equal("#", $H(Fixtures.Hash.many).inspect()); + }); + + test('#clone', function () { + var h = $H(Fixtures.Hash.many); + assert.hashEqual(h, h.clone()); + assert.isInstanceOf(h.clone(), Hash); + assert.notStrictEqual(h, h.clone()); + }); + + test('#toJSON', function () { + assert.equal('{\"b\":[null,false,true,null],\"c\":{\"a\":\"hello!\"}}', + Object.toJSON({b: [undefined, false, true, undefined], c: {a: 'hello!'}})); + }); + + test('ability to contain any key', function () { + var h = $H({ _each: 'E', map: 'M', keys: 'K', pluck: 'P', unset: 'U' }); + assert.enumEqual($w('_each keys map pluck unset'), h.keys().sort()); + assert.equal('U', h.unset('unset')); + assert.hashEqual({ _each: 'E', map: 'M', keys: 'K', pluck: 'P' }, h); + }); + + test('#toTemplateReplacements', function () { + var template = new Template("#{a} #{b}"), hash = $H({ a: "hello", b: "world" }); + assert.equal("hello world", template.evaluate(hash.toObject())); + assert.equal("hello world", template.evaluate(hash)); + assert.equal("hello", "#{a}".interpolate(hash)); + }); + + test("don't iterate over shadowed properties", function () { + // redundant now that object is systematically cloned. + var FooMaker = function(value) { + this.key = value; + }; + FooMaker.prototype.key = 'foo'; + var foo = new FooMaker('bar'); + assert.equal("key=bar", new Hash(foo).toQueryString()); + assert.equal("key=bar", new Hash(new Hash(foo)).toQueryString()); + }); + + test('#each', function () { + var h = $H({a:1, b:2}); + var result = []; + h.each(function(kv, i){ + result.push(i); + }); + assert.enumEqual([0,1], result); + }); + + +}); + + + + + + + + + + diff --git a/test/unit/tests/layout.test.js b/test/unit/tests/layout.test.js new file mode 100644 index 000000000..d0752b3ab --- /dev/null +++ b/test/unit/tests/layout.test.js @@ -0,0 +1,555 @@ +function isDisplayed(element) { + var originalElement = element; + + while (element && element.parentNode) { + var display = element.getStyle('display'); + if (display === 'none') { + return false; + } + element = $(element.parentNode); + } + return true; +} + +var documentViewportProperties = null; + +var RESIZE_DISABLED = false; + +suite("Layout", function(){ + this.name = 'layout'; + + setup(function () { + + if (documentViewportProperties) return; + + // Based on properties check from http://www.quirksmode.org/viewport/compatibility.html. + + documentViewportProperties = { + properties: [ + 'self.pageXOffset', 'self.pageYOffset', + 'self.screenX', 'self.screenY', + 'self.innerHeight', 'self.innerWidth', + 'self.outerHeight', 'self.outerWidth', + 'self.screen.height', 'self.screen.width', + 'self.screen.availHeight', 'self.screen.availWidth', + 'self.screen.availTop', 'self.screen.availLeft', + 'self.screen.Top', 'self.screen.Left', + 'self.screenTop', 'self.screenLeft', + 'document.body.clientHeight', 'document.body.clientWidth', + 'document.body.scrollHeight', 'document.body.scrollWidth', + 'document.body.scrollLeft', 'document.body.scrollTop', + 'document.body.offsetHeight', 'document.body.offsetWidth', + 'document.body.offsetTop', 'document.body.offsetLeft' + ].inject([], function (properties, prop) { + if (!self.screen && prop.include('self.screen')) return; + if (!document.body && prop.include('document.body')) return; + + properties.push(prop); + + if (prop.include('body') && document.documentElement) { + properties.push(prop.sub('.body', '.documentElement')); + } + + return properties; + }), + + inspect: function () { + var props = []; + this.properties.each(function (prop) { + if (eval(prop)) props[prop] = eval(prop); + }, this); + return props; + } + }; + }); + + + test("preCompute argument of layout", function() { + var preComputedLayout = $('box1').getLayout(true), + normalLayout = $('box1').getLayout(); + + // restore normal get method from Hash object + preComputedLayout.get = Hash.prototype.get; + + Element.Layout.PROPERTIES.each(function(key) { + assert.equal(normalLayout.get(key), preComputedLayout.get(key), key); + }); + }); + + test("layout on absolutely-positioned elements", function() { + var layout = $('box1').getLayout(); + + assert.equal(242, layout.get('width'), 'width' ); + assert.equal(555, layout.get('height'), 'height'); + + assert.equal(3, layout.get('border-left'), 'border-left'); + assert.equal(10, layout.get('padding-top'), 'padding-top'); + assert.equal(1020, layout.get('top'), 'top'); + + assert.equal(25, layout.get('left'), 'left'); + }); + + test("layout on elements with display: none and exact width", function() { + var layout = $('box2').getLayout(); + + assert(!isDisplayed($('box2')), 'box should be hidden'); + + assert.equal(500, layout.get('width'), 'width'); + assert.equal( 3, layout.get('border-right'), 'border-right'); + assert.equal( 10, layout.get('padding-bottom'), 'padding-bottom'); + assert.equal(526, layout.get('border-box-width'), 'border-box-width'); + + assert(!isDisplayed($('box2')), 'box should still be hidden'); + }); + + test("layout on elements with negative margins", function() { + var layout = $('box_with_negative_margins').getLayout(); + + assert.equal(-10, layout.get('margin-top') ); + assert.equal( -3, layout.get('margin-left') ); + assert.equal( 2, layout.get('margin-right')); + }); + + test("layout on elements with display: none and width: auto", function() { + var layout = $('box3').getLayout(); + + assert(!isDisplayed($('box3')), 'box should be hidden'); + + assert.equal(364, layout.get('width'), 'width'); + assert.equal(400, layout.get('margin-box-width'), 'margin-box-width'); + assert.equal(390, layout.get('border-box-width'), 'border-box-width'); + assert.equal(3, layout.get('border-right'), 'border-top'); + assert.equal(10, layout.get('padding-bottom'), 'padding-right'); + + // Ensure that we cleaned up after ourselves. + assert(!isDisplayed($('box3')), 'box should still be hidden'); + }); + + test("layout on elements with display: none ancestors",function() { + var layout = $('box4').getLayout(); + + assert(!isDisplayed($('box4')), 'box should be hidden'); + + // Width and height values are nonsensical for deeply-hidden elements. + assert.equal(0, layout.get('width'), 'width of a deeply-hidden element should be 0'); + assert.equal(0, layout.get('margin-box-height'), 'height of a deeply-hidden element should be 0'); + + // But we can still get meaningful values for other measurements. + assert.equal(0, layout.get('border-right'), 'border-top'); + assert.equal(13, layout.get('padding-bottom'), 'padding-right'); + + // Ensure that we cleaned up after ourselves. + assert(!isDisplayed($('box4')), 'box should still be hidden'); + }); + + test("positioning on absolutely-positioned elements", function() { + var layout = $('box5').getLayout(); + + assert.equal(30, layout.get('top'), 'top'); + assert.equal(60, layout.get('right'), 'right (percentage value)'); + + assert.equal(340, layout.get('left'), 'left'); + }); + + test("positioning on absolutely-positioned element with top=0 and left=0", function() { + var layout = $('box6').getLayout(); + + assert.equal(0, layout.get('top'), 'top'); + assert.strictEqual($('box6_parent'), $('box6').getOffsetParent()); + }); + + test("layout on statically-positioned element with percentage width", function() { + var layout = $('box7').getLayout(); + + assert.equal(150, layout.get('width')); + }); + + test("layout on absolutely-positioned element with percentage width", function() { + var layout = $('box8').getLayout(); + + assert.equal(150, layout.get('width')); + }); + + test("layout on fixed-position element with percentage width", function() { + var viewportWidth = document.viewport.getWidth(); + var layout = $('box9').getLayout(); + + function assertNear(v1, v2, message) { + var abs = Math.abs(v1 - v2); + assert(abs <= 1, message + ' (actual: ' + v1 + ', ' + v2 + ')'); + } + + // With percentage widths, we'll occasionally run into rounding + // discrepancies. Assert that the values agree to within 1 pixel. + var vWidth = viewportWidth / 4, eWidth = $('box9').measure('width'); + assertNear.call(this, vWidth, eWidth, 'width (visible)'); + + $('box9').hide(); + assertNear.call(this, vWidth, $('box9').measure('width'), 'width (hidden)'); + $('box9').show(); + }); + + test("#toCSS, #toObject, #toHash", function() { + var layout = $('box6').getLayout(); + var top = layout.get('top'); + + var cssObject = layout.toCSS('top'); + + assert('top' in cssObject, + "layout object should have 'top' property"); + + cssObject = layout.toCSS('top left bottom'); + + $w('top left bottom').each( function(prop) { + assert(prop in cssObject, "layout object should have '" + + prop + "' property"); + }, this); + + var obj = layout.toObject('top'); + assert('top' in obj, + "object should have 'top' property"); + }); + + test("dimensions on absolutely-positioned, hidden elements", function() { + var layout = $('box10').getLayout(); + + assert.equal(278, layout.get('width'), 'width' ); + assert.equal(591, layout.get('height'), 'height'); + }); + + // ELEMENT METHODS + + suite('Element', function () { + + test('#makeClipping, #undoClipping', function () { + var chained = document.createElement('DIV'); + assert.equal(chained, chained.makeClipping()); + assert.equal(chained, chained.makeClipping()); + assert.equal(chained, chained.makeClipping().makeClipping()); + + assert.equal(chained, chained.undoClipping()); + assert.equal(chained, chained.undoClipping()); + assert.equal(chained, chained.undoClipping().makeClipping()); + + ['hidden','visible','scroll'].each( function(overflowValue) { + var element = $('element_with_'+overflowValue+'_overflow'); + + assert.equal(overflowValue, element.getStyle('overflow')); + element.makeClipping(); + assert.equal('hidden', element.getStyle('overflow')); + element.undoClipping(); + assert.equal(overflowValue, element.getStyle('overflow')); + }); + }); + + test('#getHeight', function () { + assert.strictEqual(100, $('dimensions-visible').getHeight()); + assert.strictEqual(100, $('dimensions-display-none').getHeight()); + }); + + test('#getWidth', function () { + assert.strictEqual(200, $('dimensions-visible').getWidth(), '#dimensions-visible'); + assert.strictEqual(200, $('dimensions-display-none').getWidth(), '#dimensions-display-none'); + }); + + test('#getDimensions', function () { + assert.strictEqual(100, $('dimensions-visible').getDimensions().height); + assert.strictEqual(200, $('dimensions-visible').getDimensions().width); + assert.strictEqual(100, $('dimensions-display-none').getDimensions().height); + assert.strictEqual(200, $('dimensions-display-none').getDimensions().width); + + assert.strictEqual(100, $('dimensions-visible-pos-rel').getDimensions().height); + assert.strictEqual(200, $('dimensions-visible-pos-rel').getDimensions().width); + assert.strictEqual(100, $('dimensions-display-none-pos-rel').getDimensions().height); + assert.strictEqual(200, $('dimensions-display-none-pos-rel').getDimensions().width); + + assert.strictEqual(100, $('dimensions-visible-pos-abs').getDimensions().height); + assert.strictEqual(200, $('dimensions-visible-pos-abs').getDimensions().width); + assert.strictEqual(100, $('dimensions-display-none-pos-abs').getDimensions().height); + assert.strictEqual(200, $('dimensions-display-none-pos-abs').getDimensions().width); + + // known failing issue + // assert($('dimensions-nestee').getDimensions().width <= 500, 'check for proper dimensions of hidden child elements'); + + $('dimensions-td').hide(); + assert.strictEqual(100, $('dimensions-td').getDimensions().height); + assert.strictEqual(200, $('dimensions-td').getDimensions().width); + $('dimensions-td').show(); + + $('dimensions-tr').hide(); + assert.strictEqual(100, $('dimensions-tr').getDimensions().height); + assert.strictEqual(200, $('dimensions-tr').getDimensions().width); + $('dimensions-tr').show(); + + $('dimensions-table').hide(); + assert.strictEqual(100, $('dimensions-table').getDimensions().height); + assert.strictEqual(200, $('dimensions-table').getDimensions().width); + }); + + test('#positionedOffset', function () { + assert.enumEqual([10,10], + $('body_absolute').positionedOffset(), '#body_absolute'); + assert.enumEqual([10,10], + $('absolute_absolute').positionedOffset(), '#absolute_absolute'); + assert.enumEqual([10,10], + $('absolute_relative').positionedOffset(), '#absolute_relative'); + assert.enumEqual([0,10], + $('absolute_relative_undefined').positionedOffset(), '#absolute_relative_undefined'); + assert.enumEqual([10,10], + $('absolute_fixed_absolute').positionedOffset(), '#absolute_fixed_absolute'); + + var afu = $('absolute_fixed_undefined'); + assert.enumEqual([afu.offsetLeft, afu.offsetTop], + afu.positionedOffset(), '#absolute_fixed_undefined'); + + var element = new Element('div'), offset = element.positionedOffset(); + assert.enumEqual([0,0], offset, 'new element'); + assert.strictEqual(0, offset.top, 'new element top'); + assert.strictEqual(0, offset.left, 'new element left'); + }); + + test('#cumulativeOffset', function () { + var element = new Element('div'), offset = element.cumulativeOffset(); + assert.enumEqual([0,0], offset, 'new element'); + assert.strictEqual(0, offset.top, 'new element top'); + assert.strictEqual(0, offset.left, 'new element left'); + + var innerEl = new Element('div'), outerEl = new Element('div'); + outerEl.appendChild(innerEl); + assert.enumEqual([0,0], innerEl.cumulativeOffset(), 'new inner element'); + }); + + test('#viewportOffset', function () { + window.scrollTo(0, 0); + + assert.enumEqual([10, 10], + $('body_absolute').viewportOffset()); + assert.enumEqual([20,20], + $('absolute_absolute').viewportOffset()); + assert.enumEqual([20,20], + $('absolute_relative').viewportOffset()); + assert.enumEqual([20,30], + $('absolute_relative_undefined').viewportOffset()); + var element = new Element('div'), offset = element.viewportOffset(); + assert.enumEqual([0,0], offset); + assert.strictEqual(0, offset.top); + assert.strictEqual(0, offset.left); + }); + + test('#getOffsetParent', function () { + assert.equal('body_absolute', $('absolute_absolute').getOffsetParent().id, + '#body_absolute should be parent of #absolute_absolute'); + assert.equal('body_absolute', $('absolute_relative').getOffsetParent().id, + '#body_absolute should be parent of #absolute_relative'); + assert.equal('absolute_relative', $('inline').getOffsetParent().id, + '#absolute_relative should be parent of #inline'); + assert.equal('absolute_relative', $('absolute_relative_undefined').getOffsetParent().id, + '#absolute_relative should be parent of #absolute_relative_undefined'); + + assert.equal(document.body, new Element('div').getOffsetParent(), + 'body should be parent of unattached element'); + + [document, document.body, document.documentElement].each (function(node) { + assert.equal(document.body, Element.getOffsetParent(node)); + }); + }); + + test('#absolutize', function () { + $('notInlineAbsoluted', 'inlineAbsoluted').each(function(elt) { + if ('_originalLeft' in elt) delete elt._originalLeft; + elt.absolutize(); + assert.isUndefined(elt._originalLeft, 'absolutize() did not detect absolute positioning'); + }, this); + // invoking on "absolute" positioned element should return element + var element = $('absolute_fixed_undefined').setStyle({position: 'absolute'}); + assert.equal(element, element.absolutize()); + }); + + test('#relativize', function () { + // invoking on "relative" positioned element should return element + var element = $('absolute_fixed_undefined').setStyle({ + position: 'relative' }); + assert.equal(element, element.relativize()); + }); + + test('#clonePosition (when scrolling the page)', function() { + var opts = { offsetTop: 20, offsetLeft: 0, setWidth: false, setHeight: false }; + + // Before scroll. + $('sub_menu').clonePosition($('main_menu'), opts); + var before = $('sub_menu').viewportOffset().top - $('main_menu').viewportOffset().top; + + // Reset to original position. + $('sub_menu').setStyle({ + position: "absolute", + top: "250px", + left: "250px" + }); + scrollTo(0, 300); + + // After scroll. + $('sub_menu').clonePosition($('main_menu'), opts); + var after = $('sub_menu').viewportOffset()['top'] - $('main_menu').viewportOffset()['top']; + + assert.equal(before, after); + }); + + test('#clonePosition (when element is absolutely positioned and has a non-body offset parent)', function () { + var opts = { offsetTop: 20, offsetLeft: 0, setWidth: false, setHeight: false }; + + var subMenu = $('sub_menu_2'); + var mainMenu = $('main_menu_2'); + + subMenu.clonePosition(mainMenu, opts); + var offset = subMenu.viewportOffset().top - mainMenu.viewportOffset().top; + + assert.equal(offset, 20); + + scrollTo(0, 300); + + subMenu.clonePosition(mainMenu, opts); + offset = subMenu.viewportOffset().top - mainMenu.viewportOffset().top; + assert.equal(offset, 20); + }); + + test('#clonePosition (when element has fixed position)', function () { + var opts = { offsetTop: 20, offsetLeft: 0, setWidth: false, setHeight: false }; + + var subMenu = $('sub_menu_3'); + var mainMenu = $('main_menu_3'); + + subMenu.clonePosition(mainMenu, opts); + var offset = subMenu.viewportOffset().top - mainMenu.viewportOffset().top; + + assert.equal(offset, 20); + + scrollTo(0, 300); + + subMenu.clonePosition(mainMenu, opts); + offset = subMenu.viewportOffset().top - mainMenu.viewportOffset().top; + assert.equal(offset, 20); + + }); + + test('#clonePosition (when elements have the same size)', function() { + var source = $('clone_position_source'); + var target = $('clone_position_target'); + + target.clonePosition(source, { + setHeight: false, + offsetTop: source.offsetHeight + }); + + assert.equal(source.getWidth(), target.getWidth()); + assert.equal(source.getHeight(), target.getHeight()); + }); + + }); // Element + + suite('document.viewport', function () { + + test('#getDimensions', function (done) { + this.timeout(5000); + var original = document.viewport.getDimensions(); + + try { + window.resizeTo(800, 600); + } catch (e) { + info("Can't resize."); + + return done(); + } + wait(1000, done, function() { + var before = document.viewport.getDimensions(); + + var delta = { width: 800 - before.width, height: 600 - before.height }; + + window.resizeBy(50, 50); + wait(1000, done, function() { + var after = document.viewport.getDimensions(); + + // Assume that JavaScript window resizing is disabled if before width + // and after width are the same. + if (before.width === after.width) { + RESIZE_DISABLED = true; + info("SKIPPING REMAINING TESTS (JavaScript window resizing disabled)"); + return done(); + } + + + assert.equal( + before.width + 50, after.width, + "NOTE: YOU MUST ALLOW JAVASCRIPT TO RESIZE YOUR WINDOW FOR THIS TEST TO PASS" + ); + assert.equal( + before.height + 50, after.height, + "NOTE: YOU MUST ALLOW JAVASCRIPT TO RESIZE YOUR WINDOW FOR THIS TEST TO PASS" + ); + + wait(1000, done, function() { + // Restore original dimensions. + window.resizeTo( + original.width + delta.width, + original.height + delta.height + ); + done(); + }); + }); + }); + }); + + test('#getDimensions (should not affect document properties)', function () { + // No properties on the document should be affected when resizing + // an absolutely-positioned (0,0) element to viewport dimensions. + var vd = document.viewport.getDimensions(); + + var before = documentViewportProperties.inspect(); + $('elementToViewportDimensions').setStyle({ height: vd.height + 'px', width: vd.width + 'px' }).show(); + var after = documentViewportProperties.inspect(); + $('elementToViewportDimensions').hide(); + + documentViewportProperties.properties.each(function(prop) { + assert.equal(before[prop], after[prop], prop + ' was affected'); + }); + }); + + test('#getScrollOffsets', function (done) { + this.timeout(5000); + var original = document.viewport.getDimensions(); + + window.scrollTo(0, 0); + assert.equal(0, document.viewport.getScrollOffsets().top); + + window.scrollTo(0, 35); + assert.equal(35, document.viewport.getScrollOffsets().top); + + if (RESIZE_DISABLED) { + info("SKIPPING REMAINING TESTS (JavaScript window resizing disabled)"); + done(); + return; + } + + window.resizeTo(200, 650); + + wait(1000, done, function() { + var before = document.viewport.getDimensions(); + var delta = { width: 200 - before.width, height: 650 - before.height }; + + window.scrollTo(25, 35); + assert.equal(25, document.viewport.getScrollOffsets().left, + "NOTE: YOU MUST ALLOW JAVASCRIPT TO RESIZE YOUR WINDOW FOR THESE TESTS TO PASS"); + + wait(1000, done, function() { + // Restore original dimensions. + window.resizeTo( + original.width + delta.width, + original.height + delta.height + ); + done(); + }); + }); + }); + + }); // document.viewport + +}); diff --git a/test/unit/tests/number.test.js b/test/unit/tests/number.test.js new file mode 100644 index 000000000..fe4042a4c --- /dev/null +++ b/test/unit/tests/number.test.js @@ -0,0 +1,41 @@ + +suite('Number', function () { + this.name = 'number'; + + test('math methods', function () { + assert.equal(1, (0.9).round()); + assert.equal(-2, (-1.9).floor()); + assert.equal(-1, (-1.9).ceil()); + + $w('abs floor round ceil').each(function(method) { + assert.equal(Math[method](Math.PI), Math.PI[method]()); + }, this); + }); + + test('#toColorPart', function () { + assert.equal('00', (0).toColorPart()); + assert.equal('0a', (10).toColorPart()); + assert.equal('ff', (255).toColorPart()); + }); + + test('#toPaddedString', function () { + assert.equal('00', (0).toPaddedString(2, 16)); + assert.equal('0a', (10).toPaddedString(2, 16)); + assert.equal('ff', (255).toPaddedString(2, 16)); + assert.equal('000', (0).toPaddedString(3)); + assert.equal('010', (10).toPaddedString(3)); + assert.equal('100', (100).toPaddedString(3)); + assert.equal('1000', (1000).toPaddedString(3)); + }); + + test('#times', function () { + var results = []; + (5).times(function(i) { results.push(i); }); + assert.enumEqual($R(0, 4), results); + + results = []; + (5).times(function(i) { results.push(i * this.i); }, { i: 2 }); + assert.enumEqual([0, 2, 4, 6, 8], results); + }); + +}); diff --git a/test/unit/tests/object.test.js b/test/unit/tests/object.test.js new file mode 100644 index 000000000..a32c53b9d --- /dev/null +++ b/test/unit/tests/object.test.js @@ -0,0 +1,231 @@ +var Person = function(name){ + this.name = name; +}; + +Person.prototype.toJSON = function() { + return '-' + this.name; +}; + +/// + +suite('Object', function () { + this.name = 'object'; + + test('.extend', function () { + var object = {foo: 'foo', bar: [1, 2, 3]}; + assert.strictEqual(object, Object.extend(object)); + assert.hashEqual({foo: 'foo', bar: [1, 2, 3]}, object); + assert.strictEqual(object, Object.extend(object, {bla: 123})); + assert.hashEqual({foo: 'foo', bar: [1, 2, 3], bla: 123}, object); + assert.hashEqual({foo: 'foo', bar: [1, 2, 3], bla: null}, + Object.extend(object, {bla: null})); + }); + + test('.toQueryString', function () { + assert.equal('a=A&b=B&c=C&d=D%23', Object.toQueryString({a: 'A', b: 'B', c: 'C', d: 'D#'})); + }); + + test('.clone', function () { + var object = {foo: 'foo', bar: [1, 2, 3]}; + assert.notStrictEqual(object, Object.clone(object)); + assert.hashEqual(object, Object.clone(object)); + assert.hashEqual({}, Object.clone()); + var clone = Object.clone(object); + delete clone.bar; + assert.hashEqual({foo: 'foo'}, clone, + "Optimizing Object.clone perf using prototyping doesn't allow properties to be deleted."); + }); + + test('.keys', function () { + assert.enumEqual([], Object.keys({})); + assert.enumEqual(['bar', 'foo'], Object.keys({foo: 'foo', bar: 'bar'}).sort()); + function Foo() { this.bar = 'bar'; } + Foo.prototype.foo = 'foo'; + assert.enumEqual(['bar'], Object.keys(new Foo())); + assert.raise('TypeError', function(){ Object.keys(); }); + + var obj = { + foo: 'bar', + baz: 'thud', + toString: function() { return '1'; }, + valueOf: function() { return 1; } + }; + + assert.equal(4, Object.keys(obj).length, 'DontEnum properties should be included in Object.keys'); + }); + + test('.inspect', function () { + assert.equal('undefined', Object.inspect()); + assert.equal('undefined', Object.inspect(undefined)); + assert.equal('null', Object.inspect(null)); + assert.equal("'foo\\\\b\\\'ar'", Object.inspect('foo\\b\'ar')); + assert.equal('[]', Object.inspect([])); + assert.nothingRaised(function() { Object.inspect(window.Node); }); + }); + + test('.toJSON', function () { + assert.isUndefined(Object.toJSON(undefined)); + assert.isUndefined(Object.toJSON(Prototype.K)); + assert.equal('\"\"', Object.toJSON('')); + assert.equal('\"test\"', Object.toJSON('test')); + assert.equal('null', Object.toJSON(Number.NaN)); + assert.equal('0', Object.toJSON(0)); + assert.equal('-293', Object.toJSON(-293)); + assert.equal('[]', Object.toJSON([])); + assert.equal('[\"a\"]', Object.toJSON(['a'])); + assert.equal('[\"a\",1]', Object.toJSON(['a', 1])); + assert.equal('[\"a\",{\"b\":null}]', Object.toJSON(['a', {'b': null}])); + assert.equal('{\"a\":\"hello!\"}', Object.toJSON({a: 'hello!'})); + assert.equal('{}', Object.toJSON({})); + assert.equal('{}', Object.toJSON({a: undefined, b: undefined, c: Prototype.K})); + assert.equal('{\"b\":[null,false,true,null],\"c\":{\"a\":\"hello!\"}}', + Object.toJSON({'b': [undefined, false, true, undefined], c: {a: 'hello!'}})); + assert.equal('{\"b\":[null,false,true,null],\"c\":{\"a\":\"hello!\"}}', + Object.toJSON($H({'b': [undefined, false, true, undefined], c: {a: 'hello!'}}))); + assert.equal('true', Object.toJSON(true)); + assert.equal('false', Object.toJSON(false)); + assert.equal('null', Object.toJSON(null)); + var sam = new Person('sam'); + assert.equal('"-sam"', Object.toJSON(sam)); + }); + + test('.toHTML', function () { + assert.strictEqual('', Object.toHTML()); + assert.strictEqual('', Object.toHTML('')); + assert.strictEqual('', Object.toHTML(null)); + assert.strictEqual('0', Object.toHTML(0)); + assert.strictEqual('123', Object.toHTML(123)); + assert.equal('hello world', Object.toHTML('hello world')); + assert.equal('hello world', Object.toHTML({toHTML: function() { return 'hello world'; }})); + }); + + test('.isArray', function () { + assert(Object.isArray([])); + assert(Object.isArray([0])); + assert(Object.isArray([0, 1])); + assert(!Object.isArray({})); + assert(!Object.isArray($('object-test-list').childNodes)); + assert(!Object.isArray()); + assert(!Object.isArray('')); + assert(!Object.isArray('foo')); + assert(!Object.isArray(0)); + assert(!Object.isArray(1)); + assert(!Object.isArray(null)); + assert(!Object.isArray(true)); + assert(!Object.isArray(false)); + assert(!Object.isArray(undefined)); + }); + + test('.isHash', function () { + assert(Object.isHash($H())); + assert(Object.isHash(new Hash())); + assert(!Object.isHash({})); + assert(!Object.isHash(null)); + assert(!Object.isHash()); + assert(!Object.isHash('')); + assert(!Object.isHash(2)); + assert(!Object.isHash(false)); + assert(!Object.isHash(true)); + assert(!Object.isHash([])); + }); + + test('.isElement', function () { + assert(Object.isElement(document.createElement('div'))); + assert(Object.isElement(new Element('div'))); + assert(Object.isElement($('object-test'))); + assert(!Object.isElement(document.createTextNode('bla'))); + + // falsy variables should not mess up return value type + assert.strictEqual(false, Object.isElement(0)); + assert.strictEqual(false, Object.isElement('')); + assert.strictEqual(false, Object.isElement(NaN)); + assert.strictEqual(false, Object.isElement(null)); + assert.strictEqual(false, Object.isElement(undefined)); + }); + + test('.isFunction', function () { + assert(Object.isFunction(function() { })); + assert(Object.isFunction(Class.create())); + + assert(!Object.isFunction("a string")); + assert(!Object.isFunction($(document.createElement('div')))); + assert(!Object.isFunction([])); + assert(!Object.isFunction({})); + assert(!Object.isFunction(0)); + assert(!Object.isFunction(false)); + assert(!Object.isFunction(undefined)); + assert(!Object.isFunction(/xyz/), 'regular expressions are not functions'); + }); + + test('.isString', function () { + assert(!Object.isString(function() { })); + assert(Object.isString("a string")); + assert(Object.isString(new String("a string"))); + assert(!Object.isString(0)); + assert(!Object.isString([])); + assert(!Object.isString({})); + assert(!Object.isString(false)); + assert(!Object.isString(undefined)); + assert(!Object.isString(document), 'host objects should return false rather than throw exceptions'); + }); + + test('.isNumber', function () { + assert(Object.isNumber(0)); + assert(Object.isNumber(1.0)); + assert(Object.isNumber(new Number(0))); + assert(Object.isNumber(new Number(1.0))); + assert(!Object.isNumber(function() { })); + assert(!Object.isNumber({ test: function() { return 3; } })); + assert(!Object.isNumber("a string")); + assert(!Object.isNumber([])); + assert(!Object.isNumber({})); + assert(!Object.isNumber(false)); + assert(!Object.isNumber(undefined)); + assert(!Object.isNumber(document), 'host objects should return false rather than throw exceptions'); + }); + + test('.isDate', function () { + var d = new Date(); + assert(Object.isDate(d), 'constructor with no arguments'); + assert(Object.isDate(new Date(0)), 'constructor with milliseconds'); + assert(Object.isDate(new Date(1995, 11, 17)), 'constructor with Y, M, D'); + assert(Object.isDate(new Date(1995, 11, 17, 3, 24, 0)), 'constructor with Y, M, D, H, M, S'); + assert(Object.isDate(new Date(Date.parse("Dec 25, 1995"))), 'constructor with result of Date.parse'); + + assert(!Object.isDate(d.valueOf()), 'Date#valueOf returns a number'); + assert(!Object.isDate(function() { })); + assert(!Object.isDate(0)); + assert(!Object.isDate("a string")); + assert(!Object.isDate([])); + assert(!Object.isDate({})); + assert(!Object.isDate(false)); + assert(!Object.isDate(undefined)); + assert(!Object.isDate(document), 'host objects should return false rather than throw exceptions'); + }); + + test('.isUndefined', function () { + assert(Object.isUndefined(undefined)); + assert(!Object.isUndefined(null)); + assert(!Object.isUndefined(false)); + assert(!Object.isUndefined(0)); + assert(!Object.isUndefined("")); + assert(!Object.isUndefined(function() { })); + assert(!Object.isUndefined([])); + assert(!Object.isUndefined({})); + }); + + test('should not extend Object.prototype', function () { + // for-in is supported with objects + var iterations = 0, obj = { a: 1, b: 2, c: 3 }, property; + for (property in obj) iterations++; + assert.equal(3, iterations); + + // for-in is not supported with arrays + iterations = 0; + var arr = [1,2,3]; + for (property in arr) iterations++; + assert(iterations > 3); + }); + + +}); diff --git a/test/unit/tests/periodical_executer.test.js b/test/unit/tests/periodical_executer.test.js new file mode 100644 index 000000000..100e82a71 --- /dev/null +++ b/test/unit/tests/periodical_executer.test.js @@ -0,0 +1,39 @@ + +suite('PeriodicalExecuter', function () { + this.name = 'periodical_executer'; + + test('#stop', function (done) { + var peEventCount = 0; + function peEventFired(pe) { + if (++peEventCount > 2) pe.stop(); + } + + // peEventFired will stop the PeriodicalExecuter after 3 callbacks + new PeriodicalExecuter(peEventFired, 0.05); + + wait(600, done, function() { + assert.equal(3, peEventCount); + done(); + }); + }); + + test('#onTimerEvent', function () { + var pe = { + onTimerEvent: PeriodicalExecuter.prototype.onTimerEvent, + execute: function() { + assert(pe.currentlyExecuting); + } + }; + + pe.onTimerEvent(); + assert(!pe.currentlyExecuting); + + pe.execute = function() { + assert(pe.currentlyExecuting); + throw new Error(); + }; + assert.raise('Error', pe.onTimerEvent.bind(pe)); + assert(!pe.currentlyExecuting); + }); + +}); diff --git a/test/unit/tests/position.test.js b/test/unit/tests/position.test.js new file mode 100644 index 000000000..fec9acd73 --- /dev/null +++ b/test/unit/tests/position.test.js @@ -0,0 +1,47 @@ + +suite('Position', function () { + this.name = 'position'; + + setup(function () { + scrollTo(0, 0); + Position.prepare(); + Position.includeScrollOffsets = false; + }); + + teardown(function () { + scrollTo(0, 0); + Position.prepare(); + Position.includeScrollOffsets = false; + }); + + test('.prepare', function () { + Position.prepare(); + assert.equal(0, Position.deltaX); + assert.equal(0, Position.deltaY); + scrollTo(20, 30); + Position.prepare(); + assert.equal(20, Position.deltaX); + assert.equal(30, Position.deltaY); + }); + + test('.within', function () { + [true, false].each(function(withScrollOffsets) { + Position.includeScrollOffsets = withScrollOffsets; + assert(!Position.within($('position_test_body_absolute'), 9, 9), 'outside left/top'); + assert(Position.within($('position_test_body_absolute'), 10, 10), 'left/top corner'); + assert(Position.within($('position_test_body_absolute'), 10, 19), 'left/bottom corner'); + assert(!Position.within($('position_test_body_absolute'), 10, 20), 'outside bottom'); + }, this); + + scrollTo(20, 30); + Position.prepare(); + Position.includeScrollOffsets = true; + assert(!Position.within($('position_test_body_absolute'), 9, 9), 'outside left/top'); + assert(Position.within($('position_test_body_absolute'), 10, 10), 'left/top corner'); + assert(Position.within($('position_test_body_absolute'), 10, 19), 'left/bottom corner'); + assert(!Position.within($('position_test_body_absolute'), 10, 20), 'outside bottom'); + }); + + + +}); diff --git a/test/unit/tests/prototype.test.js b/test/unit/tests/prototype.test.js new file mode 100644 index 000000000..52e4a6aec --- /dev/null +++ b/test/unit/tests/prototype.test.js @@ -0,0 +1,55 @@ + +suite('Prototype', function () { + this.name = 'prototype'; + + test('browser detection', function () { + var results = $H(Prototype.Browser).map(function(engine){ + return engine; + }).partition(function(engine){ + return engine[1] === true; + }); + var trues = results[0], falses = results[1]; + + var ua = navigator.userAgent; + + info('User agent string is: ' + ua); + + // It's OK for there to be two true values if we're on MobileSafari, + // since it's also a WebKit browser. + if (Prototype.Browser.MobileSafari) { + assert(trues.size() === 2, 'MobileSafari should also identify as WebKit.'); + } else { + assert(trues.size() === 0 || trues.size() === 1, + 'There should be only one or no browser detected.'); + } + + // we should have definite trues or falses here + trues.each(function(result) { + assert(result[1] === true); + }, this); + falses.each(function(result) { + assert(result[1] === false); + }, this); + + if (ua.indexOf('AppleWebKit/') > -1) { + info('Running on WebKit'); + assert(Prototype.Browser.WebKit); + } + + if (!!window.opera) { + info('Running on Opera'); + assert(Prototype.Browser.Opera); + } + + if (ua.indexOf('MSIE') > -1 && !window.opera) { + info('Running on IE'); + assert(Prototype.Browser.IE); + } + + if (ua.indexOf('Gecko') > -1 && ua.indexOf('KHTML') == -1) { + info('Running on Gecko'); + assert(Prototype.Browser.Gecko); + } + }); + +}); diff --git a/test/unit/tests/range.test.js b/test/unit/tests/range.test.js new file mode 100644 index 000000000..ede02dd60 --- /dev/null +++ b/test/unit/tests/range.test.js @@ -0,0 +1,67 @@ + +suite('Range', function () { + this.name = 'range'; + + test('#include', function () { + assert(!$R(0, 0, true).include(0)); + assert($R(0, 0, false).include(0)); + + assert($R(0, 5, true).include(0)); + assert($R(0, 5, true).include(4)); + assert(!$R(0, 5, true).include(5)); + + assert($R(0, 5, false).include(0)); + assert($R(0, 5, false).include(5)); + assert(!$R(0, 5, false).include(6)); + }); + + test('#each', function () { + var results = []; + $R(0, 0, true).each(function(value) { + results.push(value); + }); + + assert.enumEqual([], results); + + results = []; + $R(0, 3, false).each(function(value) { + results.push(value); + }); + + assert.enumEqual([0, 1, 2, 3], results); + + results = []; + $R(2, 4, true).each(function(value, index) { + results.push(index); + }); + assert.enumEqual([0, 1], results); + }); + + test('#any', function () { + assert(!$R(1, 1, true).any()); + assert($R(0, 3, false).any(function(value) { + return value == 3; + })); + }); + + test('#all', function () { + assert($R(1, 1, true).all()); + assert($R(0, 3, false).all(function(value) { + return value <= 3; + })); + }); + + test('#toArray', function () { + assert.enumEqual([], $R(0, 0, true).toArray()); + assert.enumEqual([0], $R(0, 0, false).toArray()); + assert.enumEqual([0], $R(0, 1, true).toArray()); + assert.enumEqual([0, 1], $R(0, 1, false).toArray()); + assert.enumEqual([-3, -2, -1, 0, 1, 2], $R(-3, 3, true).toArray()); + assert.enumEqual([-3, -2, -1, 0, 1, 2, 3], $R(-3, 3, false).toArray()); + }); + + test('defaults to inclusive', function () { + assert.enumEqual($R(-3,3), $R(-3,3,false)); + }); + +}); diff --git a/test/unit/tests/regexp.test.js b/test/unit/tests/regexp.test.js new file mode 100644 index 000000000..ae2d04856 --- /dev/null +++ b/test/unit/tests/regexp.test.js @@ -0,0 +1,46 @@ + +suite('RegExp', function () { + this.name = 'regexp'; + + test('#escape', function () { + assert.equal('word', RegExp.escape('word')); + assert.equal('\\/slashes\\/', RegExp.escape('/slashes/')); + assert.equal('\\\\backslashes\\\\', RegExp.escape('\\backslashes\\')); + assert.equal('\\\\border of word', RegExp.escape('\\border of word')); + + assert.equal('\\(\\?\\:non-capturing\\)', RegExp.escape('(?:non-capturing)')); + assert.equal('non-capturing', new RegExp(RegExp.escape('(?:') + '([^)]+)').exec('(?:non-capturing)')[1]); + + assert.equal('\\(\\?\\=positive-lookahead\\)', RegExp.escape('(?=positive-lookahead)')); + assert.equal('positive-lookahead', new RegExp(RegExp.escape('(?=') + '([^)]+)').exec('(?=positive-lookahead)')[1]); + + assert.equal('\\(\\?<\\=positive-lookbehind\\)', RegExp.escape('(?<=positive-lookbehind)')); + assert.equal('positive-lookbehind', new RegExp(RegExp.escape('(?<=') + '([^)]+)').exec('(?<=positive-lookbehind)')[1]); + + assert.equal('\\(\\?\\!negative-lookahead\\)', RegExp.escape('(?!negative-lookahead)')); + assert.equal('negative-lookahead', new RegExp(RegExp.escape('(?!') + '([^)]+)').exec('(?!negative-lookahead)')[1]); + + assert.equal('\\(\\?<\\!negative-lookbehind\\)', RegExp.escape('(?', new RegExp(RegExp.escape('
    ')).exec('
    ')[0]); + + assert.equal('false', RegExp.escape(false)); + assert.equal('undefined', RegExp.escape()); + assert.equal('null', RegExp.escape(null)); + assert.equal('42', RegExp.escape(42)); + + assert.equal('\\\\n\\\\r\\\\t', RegExp.escape('\\n\\r\\t')); + assert.equal('\n\r\t', RegExp.escape('\n\r\t')); + assert.equal('\\{5,2\\}', RegExp.escape('{5,2}')); + + assert.equal( + '\\/\\(\\[\\.\\*\\+\\?\\^\\=\\!\\:\\$\\{\\}\\(\\)\\|\\[\\\\\\]\\\\\\\/\\\\\\\\\\]\\)\\/g', + RegExp.escape('/([.*+?^=!:${}()|[\\]\\/\\\\])/g') + ); + }); + +}); diff --git a/test/unit/tests/selector.test.js b/test/unit/tests/selector.test.js new file mode 100644 index 000000000..ce759ae5d --- /dev/null +++ b/test/unit/tests/selector.test.js @@ -0,0 +1,405 @@ + +function reduce(arr) { + return arr.length > 1 ? arr : arr[0]; +} + + +suite('Selector', function () { + this.name = 'selector'; + + test('tag (div)', function () { + assert.enumEqual($A(document.getElementsByTagName('li')), $$('li')); + assert.enumEqual([$('strong')], $$('strong')); + assert.enumEqual([], $$('nonexistent')); + + var allNodes = $A(document.getElementsByTagName('*')).select( function(node) { + return node.tagName !== '!'; + }); + assert.enumEqual(allNodes, $$('*')); + }); + + test('ID (#some_id)', function () { + assert.enumEqual([$('fixtures')], $$('#fixtures')); + assert.enumEqual([], $$('#nonexistent')); + assert.enumEqual([$('troubleForm')], $$('#troubleForm')); + }); + + test('class (.some-class)', function () { + assert.enumEqual($('p', 'link_1', 'item_1'), $$('.first')); + assert.enumEqual([], $$('.second')); + }); + + test('tag + ID (div#some_id)', function () { + assert.enumEqual([$('strong')], $$('strong#strong')); + assert.enumEqual([], $$('p#strong')); + }); + + test('tag + class (div.some-class)', function () { + assert.enumEqual($('link_1', 'link_2'), $$('a.internal')); + assert.enumEqual([$('link_2')], $$('a.internal.highlight')); + assert.enumEqual([$('link_2')], $$('a.highlight.internal')); + assert.enumEqual([], $$('a.highlight.internal.nonexistent')); + }); + + test('id + class (#some_id.some-class)', function () { + assert.enumEqual([$('link_2')], $$('#link_2.internal')); + assert.enumEqual([$('link_2')], $$('.internal#link_2')); + assert.enumEqual([$('link_2')], $$('#link_2.internal.highlight')); + assert.enumEqual([], $$('#link_2.internal.nonexistent')); + }); + + test('tag + id + class (div#some_id.some-class)', function () { + assert.enumEqual([$('link_2')], $$('a#link_2.internal')); + assert.enumEqual([$('link_2')], $$('a.internal#link_2')); + assert.enumEqual([$('item_1')], $$('li#item_1.first')); + assert.enumEqual([], $$('li#item_1.nonexistent')); + assert.enumEqual([], $$('li#item_1.first.nonexistent')); + }); + + test('descendant combinator', function () { + assert.enumEqual($('em2', 'em', 'span'), $$('#fixtures a *')); + assert.enumEqual([$('p')], $$('div#fixtures p')); + }); + + test('combines results when multiple expressions are passed', function () { + assert.enumEqual( + $('link_1', 'link_2', 'item_1', 'item_2', 'item_3'), + $$('#p a', ' ul#list li ') + ); + }); + + test('tag + attr existence (a[href])', function () { + assert.enumEqual($$('#fixtures h1'), $$('h1[class]'), 'h1[class]'); + assert.enumEqual($$('#fixtures h1'), $$('h1[CLASS]'), 'h1[CLASS]'); + assert.enumEqual([$('item_3')], $$('li#item_3[class]'), 'li#item_3[class]'); + }); + + test('tag + attr equality (a[href="#"])', function () { + assert.enumEqual($('link_1', 'link_2', 'link_3'), $$('#fixtures a[href="#"]')); + assert.enumEqual($('link_1', 'link_2', 'link_3'), $$('#fixtures a[href=#]')); + }); + + test('tag + attr whitespace-tokenized (a[class~="internal"])', function () { + assert.enumEqual($('link_1', 'link_2'), $$('a[class~="internal"]'), "a[class~=\"internal\"]"); + assert.enumEqual($('link_1', 'link_2'), $$('a[class~=internal]'), "a[class~=internal]"); + }); + + test('attr ([href])', function () { + assert.enumEqual(document.body.select('a[href]'), document.body.select('[href]')); + assert.enumEqual($$('a[class~="internal"]'), $$('[class~=internal]')); + assert.enumEqual($$('*[id]'), $$('[id]')); + assert.enumEqual($('checked_radio', 'unchecked_radio'), $$('[type=radio]')); + assert.enumEqual($$('*[type=checkbox]'), $$('[type=checkbox]')); + assert.enumEqual($('with_title', 'commaParent'), $$('[title]')); + assert.enumEqual($$('#troubleForm *[type=radio]'), $$('#troubleForm [type=radio]')); + assert.enumEqual($$('#troubleForm *[type]'), $$('#troubleForm [type]')); + }); + + test('attr (with hyphen) ([foo-bar])', function () { + assert.enumEqual([$('attr_with_dash')], $$('[foo-bar]'), "attribute with hyphen"); + }); + + test('attr negation a[href!="#"]', function () { + assert.enumEqual($('item_2', 'item_3'), $$('#list li[id!="item_1"]')); + // assert.enumEqual([], $$('a[href!="#"]')); + }); + + test('attr (value with brackets) (input[name="brackets[5][]"])', function () { + assert.enumEqual( + $('chk_1', 'chk_2'), + $$('#troubleForm2 input[name="brackets[5][]"]') + ); + assert.enumEqual( + [$('chk_1')], + $$('#troubleForm2 input[name="brackets[5][]"]:checked') + ); + assert.enumEqual( + [$('chk_2')], + $$('#troubleForm2 input[name="brackets[5][]"][value=2]') + ); + try { + $$('#troubleForm2 input[name=brackets[5][]]'); + assert(false, 'Error not thrown'); + } catch (e) { + assert(true, 'Error thrown'); + } + }); + + test('attr (multiple) (div[style] p[id] strong)', function () { + assert.enumEqual([$('strong')], $$('div[style] p[id] strong'), 'div[style] p[id] strong'); + }); + + test('a (multiple) ([class~=external][href="#"])', function () { + assert.enumEqual([$('link_3')], $$('a[class~=external][href="#"]'), + 'a[class~=external][href="#"]'); + assert.enumEqual([], $$('a[class~=external][href!="#"]'), + 'a[class~=external][href!="#"]'); + }); + + test('.matchElements', function () { + assert.elementsMatch(Selector.matchElements($('list').descendants(), 'li'), '#item_1', '#item_2', '#item_3'); + assert.elementsMatch(Selector.matchElements($('fixtures').descendants(), 'a.internal'), '#link_1', '#link_2'); + assert.enumEqual([], Selector.matchElements($('fixtures').descendants(), 'p.last')); + assert.elementsMatch(Selector.matchElements($('fixtures').descendants(), '.inexistant, a.internal'), '#link_1', '#link_2'); + }); + + test('.findElement', function () { + assert.elementMatches(Selector.findElement($('list').descendants(), 'li'), 'li#item_1.first'); + assert.elementMatches(Selector.findElement($('list').descendants(), 'li', 1), 'li#item_2'); + assert.elementMatches(Selector.findElement($('list').descendants(), 'li#item_3'), 'li'); + assert.equal(undefined, Selector.findElement($('list').descendants(), 'em')); + }); + + test('Element#match', function () { + var span = $('dupL1'); + + // tests that should pass + assert(span.match('span')); + assert(span.match('span#dupL1')); + assert(span.match('div > span'), 'child combinator'); + assert(span.match('#dupContainer span'), 'descendant combinator'); + assert(span.match('#dupL1'), 'ID only'); + assert(span.match('span.span_foo'), 'class name 1'); + assert(span.match('span.span_bar'), 'class name 2'); + assert(span.match('span:first-child'), 'first-child pseudoclass'); + + assert(!span.match('span.span_wtf'), 'bogus class name'); + assert(!span.match('#dupL2'), 'different ID'); + assert(!span.match('div'), 'different tag name'); + assert(!span.match('span span'), 'different ancestry'); + assert(!span.match('span > span'), 'different parent'); + assert(!span.match('span:nth-child(5)'), 'different pseudoclass'); + + assert(!$('link_2').match('a[rel^=external]')); + assert($('link_1').match('a[rel^=external]')); + assert($('link_1').match('a[rel^="external"]')); + assert($('link_1').match("a[rel^='external']")); + + assert(span.match({ match: function(element) { return true; }}), 'custom selector'); + assert(!span.match({ match: function(element) { return false; }}), 'custom selector'); + }); + + test('attr (space in value) (cite[title="hello world!"])', function () { + assert.enumEqual([$('with_title')], $$('cite[title="hello world!"]')); + }); + + test('> combinator', function () { + assert.enumEqual($('link_1', 'link_2'), $$('p.first > a')); + assert.enumEqual($('father', 'uncle'), $$('div#grandfather > div')); + assert.enumEqual($('level2_1', 'level2_2'), $$('#level1>span')); + assert.enumEqual($('level2_1', 'level2_2'), $$('#level1 > span')); + assert.enumEqual($('level3_1', 'level3_2'), $$('#level2_1 > *')); + assert.enumEqual([], $$('div > #nonexistent')); + }); + + test('+ combinator', function () { + assert.enumEqual([$('uncle')], $$('div.brothers + div.brothers')); + assert.enumEqual([$('uncle')], $$('div.brothers + div')); + assert.equal($('level2_2'), reduce($$('#level2_1+span'))); + assert.equal($('level2_2'), reduce($$('#level2_1 + span'))); + assert.equal($('level2_2'), reduce($$('#level2_1 + *'))); + assert.enumEqual([], $$('#level2_2 + span')); + assert.equal($('level3_2'), reduce($$('#level3_1 + span'))); + assert.equal($('level3_2'), reduce($$('#level3_1 + *'))); + assert.enumEqual([], $$('#level3_2 + *')); + assert.enumEqual([], $$('#level3_1 + em')); + }); + + test('~ combinator', function () { + assert.enumEqual([$('list')], $$('#fixtures h1 ~ ul')); + assert.equal($('level2_2'), reduce($$('#level2_1 ~ span'))); + assert.enumEqual($('level2_2', 'level2_3'), reduce($$('#level2_1 ~ *'))); + assert.enumEqual([], $$('#level2_2 ~ span')); + assert.enumEqual([], $$('#level3_2 ~ *')); + assert.enumEqual([], $$('#level3_1 ~ em')); + assert.enumEqual([$('level3_2')], $$('#level3_1 ~ #level3_2')); + assert.enumEqual([$('level3_2')], $$('span ~ #level3_2')); + assert.enumEqual([], $$('div ~ #level3_2')); + assert.enumEqual([], $$('div ~ #level2_3')); + }); + + test('attr (weird operators)', function () { + assert.enumEqual($('father', 'uncle'), $$('div[class^=bro]'), 'matching beginning of string'); + assert.enumEqual($('father', 'uncle'), $$('div[class$=men]'), 'matching end of string'); + assert.enumEqual($('father', 'uncle'), $$('div[class*="ers m"]'), 'matching substring'); + assert.enumEqual($('level2_1', 'level2_2', 'level2_3'), $$('#level1 *[id^="level2_"]')); + assert.enumEqual($('level2_1', 'level2_2', 'level2_3'), $$('#level1 *[id^=level2_]')); + assert.enumEqual($('level2_1', 'level3_1'), $$('#level1 *[id$="_1"]')); + assert.enumEqual($('level2_1', 'level3_1'), $$('#level1 *[id$=_1]')); + assert.enumEqual($('level2_1', 'level3_2', 'level2_2', 'level2_3'), $$('#level1 *[id*="2"]')); + assert.enumEqual($('level2_1', 'level3_2', 'level2_2', 'level2_3'), $$('#level1 *[id*=2]')); + }); + + test('selectors with duplicates', function () { + assert.enumEqual($$('div div'), $$('div div').uniq()); + assert.enumEqual($('dupL2', 'dupL3', 'dupL4', 'dupL5'), $$('#dupContainer span span')); + }); + + test(':(first|last|only|nth|nth-last)-child', function () { + assert.enumEqual([$('level2_1')], $$('#level1>*:first-child')); + assert.enumEqual($('level2_1', 'level3_1', 'level_only_child'), $$('#level1 *:first-child')); + assert.enumEqual([$('level2_3')], $$('#level1>*:last-child')); + assert.enumEqual($('level3_2', 'level_only_child', 'level2_3'), $$('#level1 *:last-child')); + assert.enumEqual([$('level2_3')], $$('#level1>div:last-child')); + assert.enumEqual([$('level2_3')], $$('#level1 div:last-child')); + assert.enumEqual([], $$('#level1>div:first-child')); + assert.enumEqual([], $$('#level1>span:last-child')); + assert.enumEqual($('level2_1', 'level3_1'), $$('#level1 span:first-child')); + assert.enumEqual([], $$('#level1:first-child')); + assert.enumEqual([], $$('#level1>*:only-child')); + assert.enumEqual([$('level_only_child')], $$('#level1 *:only-child')); + assert.enumEqual([], $$('#level1:only-child')); + assert.enumEqual([$('link_2')], $$('#p *:nth-last-child(2)'), 'nth-last-child'); + assert.enumEqual([$('link_2')], $$('#p *:nth-child(3)'), 'nth-child'); + assert.enumEqual([$('link_2')], $$('#p a:nth-child(3)'), 'nth-child'); + assert.enumEqual($('item_2', 'item_3'), $$('#list > li:nth-child(n+2)')); + assert.enumEqual($('item_1', 'item_2'), $$('#list > li:nth-child(-n+2)')); + }); + + test(':(first|last|nth|nth-last)-of-type', function () { + assert.enumEqual([$('link_2')], $$('#p a:nth-of-type(2)'), 'nth-of-type'); + assert.enumEqual([$('link_1')], $$('#p a:nth-of-type(1)'), 'nth-of-type'); + assert.enumEqual([$('link_2')], $$('#p a:nth-last-of-type(1)'), 'nth-last-of-type'); + assert.enumEqual([$('link_1')], $$('#p a:first-of-type'), 'first-of-type'); + assert.enumEqual([$('link_2')], $$('#p a:last-of-type'), 'last-of-type'); + }); + + test(':not', function () { + assert.enumEqual([$('link_2')], $$('#p a:not(a:first-of-type)'), 'first-of-type'); + assert.enumEqual([$('link_1')], $$('#p a:not(a:last-of-type)'), 'last-of-type'); + assert.enumEqual([$('link_2')], $$('#p a:not(a:nth-of-type(1))'), 'nth-of-type'); + assert.enumEqual([$('link_1')], $$('#p a:not(a:nth-last-of-type(1))'), 'nth-last-of-type'); + assert.enumEqual([$('link_2')], $$('#p a:not([rel~=nofollow])'), 'attribute 1'); + assert.enumEqual([$('link_2')], $$('#p a:not(a[rel^=external])'), 'attribute 2'); + assert.enumEqual([$('link_2')], $$('#p a:not(a[rel$=nofollow])'), 'attribute 3'); + assert.enumEqual([$('em')], $$('#p a:not(a[rel$="nofollow"]) > em'), 'attribute 4'); + assert.enumEqual([$('item_2')], $$('#list li:not(#item_1):not(#item_3)'), 'adjacent :not clauses'); + assert.enumEqual([$('son')], $$('#grandfather > div:not(#uncle) #son')); + assert.enumEqual([$('em')], $$('#p a:not(a[rel$="nofollow"]) em'), 'attribute 4 + all descendants'); + assert.enumEqual([$('em')], $$('#p a:not(a[rel$="nofollow"])>em'), 'attribute 4 (without whitespace)'); + }); + + test(':enabled, :disabled, :checked', function () { + assert.enumEqual( + [$('disabled_text_field')], + $$('#troubleForm > *:disabled'), + ':disabled' + ); + assert.enumEqual( + $('troubleForm').getInputs().without($('disabled_text_field')), + $$('#troubleForm > *:enabled'), + ':enabled' + ); + assert.enumEqual( + $('checked_box', 'checked_radio'), + $$('#troubleForm *:checked'), + ':checked' + ); + }); + + test('identical results from equivalent selectors', function () { + assert.enumEqual($$('div.brothers'), $$('div[class~=brothers]')); + assert.enumEqual($$('div.brothers'), $$('div[class~=brothers].brothers')); + assert.enumEqual($$('div:not(.brothers)'), $$('div:not([class~=brothers])')); + assert.enumEqual($$('li ~ li'), $$('li:not(:first-child)')); + assert.enumEqual($$('ul > li'), $$('ul > li:nth-child(n)')); + assert.enumEqual($$('ul > li:nth-child(even)'), $$('ul > li:nth-child(2n)')); + assert.enumEqual($$('ul > li:nth-child(odd)'), $$('ul > li:nth-child(2n+1)')); + assert.enumEqual($$('ul > li:first-child'), $$('ul > li:nth-child(1)')); + assert.enumEqual($$('ul > li:last-child'), $$('ul > li:nth-last-child(1)')); + assert.enumEqual($$('ul > li:nth-child(n-999)'), $$('ul > li')); + assert.enumEqual($$('ul>li'), $$('ul > li')); + assert.enumEqual($$('#p a:not(a[rel$="nofollow"])>em'), $$('#p a:not(a[rel$="nofollow"]) > em')); + }); + + test('selectors that should return nothing', function () { + assert.enumEqual([], $$('span:empty > *')); + assert.enumEqual([], $$('div.brothers:not(.brothers)')); + assert.enumEqual([], $$('#level2_2 :only-child:not(:last-child)')); + assert.enumEqual([], $$('#level2_2 :only-child:not(:first-child)')); + }); + + test('$$ (separates selectors properly)', function () { + assert.enumEqual($('p', 'link_1', 'list', 'item_1', 'item_3', 'troubleForm'), $$('#list, .first,#item_3 , #troubleForm')); + assert.enumEqual($('p', 'link_1', 'list', 'item_1', 'item_3', 'troubleForm'), $$('#list, .first', '#item_3 , #troubleForm')); + assert.enumEqual($('commaParent', 'commaChild'), $$('form[title*="commas,"], input[value="#commaOne,#commaTwo"]')); + assert.enumEqual($('commaParent', 'commaChild'), $$('form[title*="commas,"]', 'input[value="#commaOne,#commaTwo"]')); + }); + + test('$$ (extends all nodes)', function () { + var element = document.createElement('div'); + (3).times(function(){ + element.appendChild(document.createElement('div')); + }); + element.setAttribute('id', 'scratch_element'); + $$('body')[0].appendChild(element); + + var results = $$('#scratch_element div'); + assert(typeof results[0].show == 'function'); + assert(typeof results[1].show == 'function'); + assert(typeof results[2].show == 'function'); + }); + + test('copied nodes get included', function () { + assert.elementsMatch( + Selector.matchElements($('counted_container').descendants(), 'div'), + 'div.is_counted' + ); + $('counted_container').innerHTML += $('counted_container').innerHTML; + assert.elementsMatch( + Selector.matchElements($('counted_container').descendants(), 'div'), 'div.is_counted', + 'div.is_counted' + ); + }); + + test('#select (on detached nodes)', function () { + var wrapper = new Element("div"); + wrapper.update("
    "); + assert.isNotNullOrUndefined(wrapper.select('[id=myTD]')[0], + 'selecting: [id=myTD]'); + assert.isNotNullOrUndefined(wrapper.select('#myTD')[0], + 'selecting: #myTD'); + assert.isNotNullOrUndefined(wrapper.select('td')[0], + 'selecting: td'); + assert($$('#myTD').length == 0, + 'should not turn up in document-rooted search'); + }); + + test('#down', function () { + var a = $('dupL4'); + var b = $('dupContainer').down('#dupL4'); + + assert.equal(a, b); + }); + + test('#down (with dot and colon)', function () { + var a = $('dupL4_dotcolon'); + var b = $('dupContainer.withdot:active').down('#dupL4_dotcolon'); + var c = $('dupContainer.withdot:active').select('#dupL4_dotcolon'); + + assert.equal(a, b); + assert.enumEqual([a], c); + }); + + test('descendant selector bugs', function () { + var el = document.createElement('div'); + el.innerHTML = '
    '; + document.body.appendChild(el); + assert.equal(2, $(el).select('ul li').length); + document.body.removeChild(el); + }); + + test('.findElement (with index when elements are not in document order)', function () { + var ancestors = $("target_1").ancestors(); + assert.equal( + $("container_2"), + Selector.findElement(ancestors, "[container], .container", 0) + ); + assert.equal( + $("container_1"), + Selector.findElement(ancestors, "[container], .container", 1) + ); + }); + + +}); diff --git a/test/unit/tests/selector_engine.test.js b/test/unit/tests/selector_engine.test.js new file mode 100644 index 000000000..7c6d36035 --- /dev/null +++ b/test/unit/tests/selector_engine.test.js @@ -0,0 +1,47 @@ + +suite('Selector engine', function () { + this.name = 'selector_engine'; + + test('.engine', function () { + assert(Prototype.Selector.engine); + }); + + test('.select', function () { + var elements = Prototype.Selector.select('.test_class'); + + assert(Object.isArray(elements)); + assert.equal(2, elements.length); + assert.equal('div_parent', elements[0].id); + assert.equal('div_child', elements[1].id); + }); + + test('.select (with context)', function () { + var elements = Prototype.Selector.select('.test_class', $('div_parent')); + + assert(Object.isArray(elements)); + assert.equal(1, elements.length); + assert.equal('div_child', elements[0].id); + }); + + test('.select (with empty result set)', function () { + var elements = Prototype.Selector.select('.non_existent'); + + assert(Object.isArray(elements)); + assert.equal(0, elements.length); + }); + + test('.match', function () { + var element = $('div_parent'); + + assert.equal(true, Prototype.Selector.match(element, '.test_class')); + assert.equal(false, Prototype.Selector.match(element, '.non_existent')); + }); + + test('.find', function () { + var elements = document.getElementsByTagName('*'), + expression = '.test_class'; + assert.equal('div_parent', Prototype.Selector.find(elements, expression).id); + assert.equal('div_child', Prototype.Selector.find(elements, expression, 1).id); + }); + +}); diff --git a/test/unit/tests/string.test.js b/test/unit/tests/string.test.js new file mode 100644 index 000000000..27ef2b696 --- /dev/null +++ b/test/unit/tests/string.test.js @@ -0,0 +1,597 @@ +var attackTarget; +var evalScriptsCounter = 0, + largeTextEscaped = '<span>test</span>', + largeTextUnescaped = 'test'; +(2048).times(function(){ + largeTextEscaped += ' ABC'; + largeTextUnescaped += ' ABC'; +}); + + +/// + +suite('String', function () { + this.name = 'string'; + + test('.interpret', function () { + assert.strictEqual('true', String.interpret(true)); + assert.strictEqual('123', String.interpret(123)); + assert.strictEqual('foo bar', String.interpret('foo bar')); + assert.strictEqual( + 'object string', + String.interpret({ toString: function (){ return 'object string'; } }) + ); + assert.strictEqual('0', String.interpret(0)); + assert.strictEqual('false', String.interpret(false)); + assert.strictEqual('', String.interpret(undefined)); + assert.strictEqual('', String.interpret(null)); + assert.strictEqual('', String.interpret('')); + }); + + test('#gsub (with replacement function)', function () { + var source = 'foo boo boz'; + + assert.equal('Foo Boo BoZ', + source.gsub(/[^o]+/, function(match) { + return match[0].toUpperCase(); + })); + assert.equal('f2 b2 b1z', + source.gsub(/o+/, function(match) { + return match[0].length; + })); + assert.equal('f0 b0 b1z', + source.gsub(/o+/, function(match) { + return match[0].length % 2; + })); + }); + + test('#gsub (with replacement string)', function () { + var source = 'foo boo boz'; + + assert.equal('foobooboz', + source.gsub(/\s+/, '')); + assert.equal(' z', + source.gsub(/(.)(o+)/, '')); + + assert.equal('ウィメンズ2007
    クルーズコレクション', + 'ウィメンズ2007\nクルーズコレクション'.gsub(/\n/,'
    ')); + assert.equal('ウィメンズ2007
    クルーズコレクション', + 'ウィメンズ2007\nクルーズコレクション'.gsub('\n','
    ')); + + assert.equal('barfbarobarobar barbbarobarobar barbbarobarzbar', + source.gsub('', 'bar')); + assert.equal('barfbarobarobar barbbarobarobar barbbarobarzbar', + source.gsub(new RegExp(''), 'bar')); + }); + + test('#gsub (with replacement template string)', function () { + var source = 'foo boo boz'; + + assert.equal('-oo-#{1}- -oo-#{1}- -o-#{1}-z', + source.gsub(/(.)(o+)/, '-#{2}-\\#{1}-')); + assert.equal('-foo-f- -boo-b- -bo-b-z', + source.gsub(/(.)(o+)/, '-#{0}-#{1}-')); + assert.equal('-oo-f- -oo-b- -o-b-z', + source.gsub(/(.)(o+)/, '-#{2}-#{1}-')); + assert.equal(' z', + source.gsub(/(.)(o+)/, '#{3}')); + }); + + test('#gsub (with troublesome characters)', function () { + assert.equal('ab', 'a|b'.gsub('|', '')); + assert.equal('ab', 'ab(?:)'.gsub('(?:)', '')); + assert.equal('ab', 'ab()'.gsub('()', '')); + assert.equal('ab', 'ab'.gsub('^', '')); + assert.equal('ab', 'a?b'.gsub('?', '')); + assert.equal('ab', 'a+b'.gsub('+', '')); + assert.equal('ab', 'a*b'.gsub('*', '')); + assert.equal('ab', 'a{1}b'.gsub('{1}', '')); + assert.equal('ab', 'a.b'.gsub('.', '')); + }); + + test('#gsub (with zero-length match)', function () { + assert.equal('ab', 'ab'.gsub('', '')); + assert.equal('a', 'a'.gsub(/b*/, 'c')); + assert.equal('abc', 'abc'.gsub(/b{0}/, '')); + }); + + test('#sub (with replacement function)', function () { + var source = 'foo boo boz'; + + assert.equal('Foo boo boz', + source.sub(/[^o]+/, function(match) { + return match[0].toUpperCase(); + }), 1); + assert.equal('Foo Boo boz', + source.sub(/[^o]+/, function(match) { + return match[0].toUpperCase(); + }, 2), 2); + assert.equal(source, + source.sub(/[^o]+/, function(match) { + return match[0].toUpperCase(); + }, 0), 0); + assert.equal(source, + source.sub(/[^o]+/, function(match) { + return match[0].toUpperCase(); + }, -1), -1); + }); + + test('#sub (with replacement string)', function () { + var source = 'foo boo boz'; + + assert.equal('oo boo boz', + source.sub(/[^o]+/, '')); + assert.equal('oooo boz', + source.sub(/[^o]+/, '', 2)); + assert.equal('-f-oo boo boz', + source.sub(/[^o]+/, '-#{0}-')); + assert.equal('-f-oo- b-oo boz', + source.sub(/[^o]+/, '-#{0}-', 2)); + }); + + test('#scan', function () { + var source = 'foo boo boz', results = []; + var str = source.scan(/[o]+/, function(match) { + results.push(match[0].length); + }); + assert.enumEqual([2, 2, 1], results); + assert.equal(source, source.scan(/x/, function () { + assert(false, 'this iterator should never get called'); + })); + assert(typeof str == 'string'); + }); + + test('#toArray', function () { + assert.enumEqual([],''.toArray()); + assert.enumEqual(['a'],'a'.toArray()); + assert.enumEqual(['a','b'],'ab'.toArray()); + assert.enumEqual(['f','o','o'],'foo'.toArray()); + }); + + test('#camelize', function () { + assert.equal('', ''.camelize()); + assert.equal('', '-'.camelize()); + assert.equal('foo', 'foo'.camelize()); + assert.equal('foo_bar', 'foo_bar'.camelize()); + assert.equal('FooBar', '-foo-bar'.camelize()); + assert.equal('FooBar', 'FooBar'.camelize()); + + assert.equal('fooBar', 'foo-bar'.camelize()); + assert.equal('borderBottomWidth', 'border-bottom-width'.camelize()); + + assert.equal('classNameTest','class-name-test'.camelize()); + assert.equal('classNameTest','className-test'.camelize()); + assert.equal('classNameTest','class-nameTest'.camelize()); + }); + + test('#capitalize', function () { + assert.equal('',''.capitalize()); + assert.equal('Ä','ä'.capitalize()); + assert.equal('A','A'.capitalize()); + assert.equal('Hello','hello'.capitalize()); + assert.equal('Hello','HELLO'.capitalize()); + assert.equal('Hello','Hello'.capitalize()); + assert.equal('Hello world','hello WORLD'.capitalize()); + }); + + test('#underscore', function () { + assert.equal('', ''.underscore()); + assert.equal('_', '-'.underscore()); + assert.equal('foo', 'foo'.underscore()); + assert.equal('foo', 'Foo'.underscore()); + assert.equal('foo_bar', 'foo_bar'.underscore()); + assert.equal('border_bottom', 'borderBottom'.underscore()); + assert.equal('border_bottom_width', 'borderBottomWidth'.underscore()); + assert.equal('border_bottom_width', 'border-Bottom-Width'.underscore()); }); + + test('#dasherize', function () { + assert.equal('', ''.dasherize()); + assert.equal('foo', 'foo'.dasherize()); + assert.equal('Foo', 'Foo'.dasherize()); + assert.equal('foo-bar', 'foo-bar'.dasherize()); + assert.equal('border-bottom-width', 'border_bottom_width'.dasherize()); + }); + + test('#truncate', function () { + var source = 'foo boo boz foo boo boz foo boo boz foo boo boz'; + assert.equal(source, source.truncate(source.length)); + assert.equal('foo boo boz foo boo boz foo...', source.truncate(0)); + assert.equal('fo...', source.truncate(5)); + assert.equal('foo b', source.truncate(5, '')); + + assert(typeof 'foo'.truncate(5) == 'string'); + assert(typeof 'foo bar baz'.truncate(5) == 'string'); + }); + + test('#strip', function () { + assert.equal('hello world', ' hello world '.strip()); + assert.equal('hello world', 'hello world'.strip()); + assert.equal('hello \n world', ' hello \n world '.strip()); + assert.equal('', ' '.strip()); + }); + + test('#stripTags', function () { + assert.equal('hello world', 'hello world'.stripTags()); + assert.equal('hello world', 'hello world'.stripTags()); + assert.equal('hello world', 'hello world'.stripTags()); + assert.equal('hello world', 'hello world'.stripTags()); + assert.equal('1\n2', '1\n2'.stripTags()); + assert.equal('one < two blah baz', 'one < two blah baz'.stripTags()); + assert.equal('hello world abc', 'hello world
    abc'.stripTags()); + assert.equal('hello world abc', 'hello world
    abc'.stripTags()); + assert.equal('hello world abc', 'hello

    world



    abc'.stripTags()); + }); + + test('#stripScripts', function () { + assert.equal('foo bar', 'foo bar'.stripScripts()); + assert.equal('foo bar', ('foo + + + + + + + + + <% @suites.each do |suite| %> + + <% end %> + + + + + + + +
    + +
    + +
    + <%= yield %> +
    + + + + + + + + + \ No newline at end of file diff --git a/test/unit/views/tests.erb b/test/unit/views/tests.erb new file mode 100644 index 000000000..d2814f8f9 --- /dev/null +++ b/test/unit/views/tests.erb @@ -0,0 +1,9 @@ + + +<% @suites.each do |suite| %> + <% if suite_has_html?(suite) %> +
    + <%= erb(:"tests/#{suite}", :locals => { :name => suite }) %> +
    + <% end %> +<% end %> \ No newline at end of file diff --git a/test/unit/fixtures/ajax.html b/test/unit/views/tests/ajax.erb similarity index 100% rename from test/unit/fixtures/ajax.html rename to test/unit/views/tests/ajax.erb diff --git a/test/unit/fixtures/array.html b/test/unit/views/tests/array.erb similarity index 100% rename from test/unit/fixtures/array.html rename to test/unit/views/tests/array.erb diff --git a/test/unit/fixtures/dom.html b/test/unit/views/tests/dom.erb similarity index 83% rename from test/unit/fixtures/dom.html rename to test/unit/views/tests/dom.erb index 99395eeca..e7c694e9f 100644 --- a/test/unit/fixtures/dom.html +++ b/test/unit/views/tests/dom.erb @@ -1,9 +1,108 @@ + +

    Scroll test

    + +
    visible
    +
    hidden
    visible
    visible
    @@ -87,7 +186,7 @@
    - + - + - +

    @@ -191,10 +290,10 @@
    FI
    FS
    -op2 -op2 -op3 -op3 +op2 +op2 +op3 +op3
    @@ -212,12 +311,12 @@
    - +

    -

    - - +

    + +

    content

    diff --git a/test/unit/fixtures/element_mixins.html b/test/unit/views/tests/element_mixins.erb similarity index 100% rename from test/unit/fixtures/element_mixins.html rename to test/unit/views/tests/element_mixins.erb diff --git a/test/unit/fixtures/enumerable.html b/test/unit/views/tests/enumerable.erb similarity index 100% rename from test/unit/fixtures/enumerable.html rename to test/unit/views/tests/enumerable.erb diff --git a/test/unit/fixtures/event.html b/test/unit/views/tests/event.erb similarity index 71% rename from test/unit/fixtures/event.html rename to test/unit/views/tests/event.erb index baa88a7d1..2c33b123c 100644 --- a/test/unit/fixtures/event.html +++ b/test/unit/views/tests/event.erb @@ -1,4 +1,4 @@ -
    +
    diff --git a/test/unit/views/tests/event_handler.erb b/test/unit/views/tests/event_handler.erb new file mode 100644 index 000000000..c103f6415 --- /dev/null +++ b/test/unit/views/tests/event_handler.erb @@ -0,0 +1,4 @@ + diff --git a/test/unit/views/tests/form.erb b/test/unit/views/tests/form.erb new file mode 100644 index 000000000..a43ea300b --- /dev/null +++ b/test/unit/views/tests/form.erb @@ -0,0 +1,148 @@ +
    +
    + +
    This is not a form element
    + + + + +
    + +
    +
    + + + + + +
    + + +
    + + + + + +
    + + +
    + + + + +
    + + + +
    +
    + +
    + + + + + +
    This is not a form element
    + + + + +
    +
    + + + +
    + +
    + +
    + + +
    +
    +

    + + +
    + +
    +

    + + + +
    + +
    +

    + + + +
    + +
    + + + + + +
    + + +
    + +
    + + + + + +
    +
    diff --git a/test/unit/views/tests/layout.erb b/test/unit/views/tests/layout.erb new file mode 100644 index 000000000..b07949786 --- /dev/null +++ b/test/unit/views/tests/layout.erb @@ -0,0 +1,380 @@ +
    +

    Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

    +
    + + + + + +
    +

    Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

    +
    + + + + + +
    +

    Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

    +
    + + + + +
    +
    + Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +
    +
    + + + + +
    +
    +
    + Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +
    +
    +
    + + + + +
    +
    +
    + Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +
    +
    +
    + + + + +
    +
    + Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +
    +
    + + + + + +
    +
    + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +
    +
    + + + + +
    +
    + Duis aute. +
    +
    + + + + +
    + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +
    + + + + +
    +

    Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

    +
    + + + +
    V
    +
    H
    +
    S
    + +
    +
    +
    +
    +
    +
    + + + + + + +
    Data
    + +
    + +
    + + + + + +
    +
    +
    +
    testtest
    +
    XYZ
    +
    +
    + foo + bar +
    +
    +
    + + +
    + + + + + + + +
    Force scroll
    + + + + + + + + + + + +
    +
    source
    +
    + +
    +
    target
    +
    + + \ No newline at end of file diff --git a/test/unit/views/tests/object.erb b/test/unit/views/tests/object.erb new file mode 100644 index 000000000..c2a7a0f3e --- /dev/null +++ b/test/unit/views/tests/object.erb @@ -0,0 +1,6 @@ +
    +
      +
    • +
    • +
    • +
    \ No newline at end of file diff --git a/test/unit/views/tests/position.erb b/test/unit/views/tests/position.erb new file mode 100644 index 000000000..803b7f550 --- /dev/null +++ b/test/unit/views/tests/position.erb @@ -0,0 +1,9 @@ +
    + +
    +
    +
    +
    testtest
    +
    +
    +
    diff --git a/test/unit/fixtures/selector.html b/test/unit/views/tests/selector.erb similarity index 93% rename from test/unit/fixtures/selector.html rename to test/unit/views/tests/selector.erb index 4cfa29ffd..4a47a1db7 100644 --- a/test/unit/fixtures/selector.html +++ b/test/unit/views/tests/selector.erb @@ -2,7 +2,7 @@

    Some title here

    This is a short blurb - with a link or + with a link or two. Or a citation.

    @@ -23,12 +23,12 @@

    Some title here

    - +
    - +
    - +
    @@ -36,7 +36,7 @@

    Some title here

    - +
    @@ -65,21 +65,21 @@

    Some title here

    -
    grandfather -
    father +
    grandfather +
    father
    son
    uncle
    -
    +
    - +
    blah
    - +
    diff --git a/test/unit/views/tests/selector_engine.erb b/test/unit/views/tests/selector_engine.erb new file mode 100644 index 000000000..4e8c2079b --- /dev/null +++ b/test/unit/views/tests/selector_engine.erb @@ -0,0 +1,4 @@ +
    +
    +
    +
    \ No newline at end of file diff --git a/vendor/legacy_selector/repository/legacy_selector.js b/vendor/legacy_selector/repository/legacy_selector.js new file mode 100644 index 000000000..288b7571c --- /dev/null +++ b/vendor/legacy_selector/repository/legacy_selector.js @@ -0,0 +1,723 @@ +/* Portions of the Prototype.LegacySelector class are derived from Jack Slocum's DomQuery, + * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style + * license. Please see http://www.yui-ext.com/ for more information. */ + +Prototype.LegacySelector = Class.create({ + initialize: function(expression) { + this.expression = expression.strip(); + + if (this.shouldUseSelectorsAPI()) { + this.mode = 'selectorsAPI'; + } else { + this.mode = "normal"; + this.compileMatcher(); + } + }, + + shouldUseSelectorsAPI: function() { + if (!Prototype.BrowserFeatures.SelectorsAPI) return false; + + if (!Prototype.LegacySelector._div) Prototype.LegacySelector._div = new Element('div'); + + // Make sure the browser treats the selector as valid. Test on an + // isolated element to minimize cost of this check. + try { + Prototype.LegacySelector._div.querySelector(this.expression); + } catch(e) { + return false; + } + + return true; + }, + + compileMatcher: function() { + var e = this.expression, ps = Prototype.LegacySelector.patterns, h = Prototype.LegacySelector.handlers, + c = Prototype.LegacySelector.criteria, le, p, m, len = ps.length, name; + + if (Prototype.LegacySelector._cache[e]) { + this.matcher = Prototype.LegacySelector._cache[e]; + return; + } + + this.matcher = ["this.matcher = function(root) {", + "var r = root, h = Prototype.LegacySelector.handlers, c = false, n;"]; + + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i = 0; i"; + } +}); + +Prototype.LegacySelector.CASE_INSENSITIVE_CLASS_NAMES = false; + +Object.extend(Prototype.LegacySelector, { + _cache: { }, + + xpath: { + descendant: "//*", + child: "/*", + adjacent: "/following-sibling::*[1]", + laterSibling: '/following-sibling::*', + tagName: function(m) { + if (m[1] == '*') return ''; + return "[local-name()='" + m[1].toLowerCase() + + "' or local-name()='" + m[1].toUpperCase() + "']"; + }, + className: "[contains(concat(' ', @class, ' '), ' #{1} ')]", + id: "[@id='#{1}']", + attrPresence: function(m) { + m[1] = m[1].toLowerCase(); + return new Template("[@#{1}]").evaluate(m); + }, + attr: function(m) { + m[1] = m[1].toLowerCase(); + m[3] = m[5] || m[6]; + return new Template(Prototype.LegacySelector.xpath.operators[m[2]]).evaluate(m); + }, + pseudo: function(m) { + var h = Prototype.LegacySelector.xpath.pseudos[m[1]]; + if (!h) return ''; + if (Object.isFunction(h)) return h(m); + return new Template(Prototype.LegacySelector.xpath.pseudos[m[1]]).evaluate(m); + }, + operators: { + '=': "[@#{1}='#{3}']", + '!=': "[@#{1}!='#{3}']", + '^=': "[starts-with(@#{1}, '#{3}')]", + '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']", + '*=': "[contains(@#{1}, '#{3}')]", + '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]", + '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]" + }, + pseudos: { + 'first-child': '[not(preceding-sibling::*)]', + 'last-child': '[not(following-sibling::*)]', + 'only-child': '[not(preceding-sibling::* or following-sibling::*)]', + 'empty': "[count(*) = 0 and (count(text()) = 0)]", + 'checked': "[@checked]", + 'disabled': "[(@disabled) and (@type!='hidden')]", + 'enabled': "[not(@disabled) and (@type!='hidden')]", + 'not': function(m) { + var e = m[6], p = Prototype.LegacySelector.patterns, + x = Prototype.LegacySelector.xpath, le, v, len = p.length, name; + + var exclusion = []; + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i = 0; i= 0)]"; + return new Template(predicate).evaluate({ + fragment: fragment, a: a, b: b }); + } + } + } + }, + + criteria: { + tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;', + className: 'n = h.className(n, r, "#{1}", c); c = false;', + id: 'n = h.id(n, r, "#{1}", c); c = false;', + attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;', + attr: function(m) { + m[3] = (m[5] || m[6]); + return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m); + }, + pseudo: function(m) { + if (m[6]) m[6] = m[6].replace(/"/g, '\\"'); + return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m); + }, + descendant: 'c = "descendant";', + child: 'c = "child";', + adjacent: 'c = "adjacent";', + laterSibling: 'c = "laterSibling";' + }, + + patterns: [ + // combinators must be listed first + // (and descendant needs to be last combinator) + { name: 'laterSibling', re: /^\s*~\s*/ }, + { name: 'child', re: /^\s*>\s*/ }, + { name: 'adjacent', re: /^\s*\+\s*/ }, + { name: 'descendant', re: /^\s/ }, + + // selectors follow + { name: 'tagName', re: /^\s*(\*|[\w\-]+)(\b|$)?/ }, + { name: 'id', re: /^#([\w\-\*]+)(\b|$)/ }, + { name: 'className', re: /^\.([\w\-\*]+)(\b|$)/ }, + { name: 'pseudo', re: /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/ }, + { name: 'attrPresence', re: /^\[((?:[\w-]+:)?[\w-]+)\]/ }, + { name: 'attr', re: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/ } + ], + + // for Prototype.LegacySelector.match and Element#match + assertions: { + tagName: function(element, matches) { + return matches[1].toUpperCase() == element.tagName.toUpperCase(); + }, + + className: function(element, matches) { + return Element.hasClassName(element, matches[1]); + }, + + id: function(element, matches) { + return element.id === matches[1]; + }, + + attrPresence: function(element, matches) { + return Element.hasAttribute(element, matches[1]); + }, + + attr: function(element, matches) { + var nodeValue = Element.readAttribute(element, matches[1]); + return nodeValue && Prototype.LegacySelector.operators[matches[2]](nodeValue, matches[5] || matches[6]); + } + }, + + handlers: { + // UTILITY FUNCTIONS + // joins two collections + concat: function(a, b) { + for (var i = 0, node; node = b[i]; i++) + a.push(node); + return a; + }, + + // marks an array of nodes for counting + mark: function(nodes) { + var _true = Prototype.emptyFunction; + for (var i = 0, node; node = nodes[i]; i++) + node._countedByPrototype = _true; + return nodes; + }, + + unmark: (function(){ + + // IE improperly serializes _countedByPrototype in (inner|outer)HTML + // due to node properties being mapped directly to attributes + var PROPERTIES_ATTRIBUTES_MAP = (function(){ + var el = document.createElement('div'), + isBuggy = false, + propName = '_countedByPrototype', + value = 'x'; + el[propName] = value; + isBuggy = (el.getAttribute(propName) === value); + el = null; + return isBuggy; + })(); + + return PROPERTIES_ATTRIBUTES_MAP ? + function(nodes) { + for (var i = 0, node; node = nodes[i]; i++) + node.removeAttribute('_countedByPrototype'); + return nodes; + } : + function(nodes) { + for (var i = 0, node; node = nodes[i]; i++) + node._countedByPrototype = void 0; + return nodes; + } + })(), + + // mark each child node with its position (for nth calls) + // "ofType" flag indicates whether we're indexing for nth-of-type + // rather than nth-child + index: function(parentNode, reverse, ofType) { + parentNode._countedByPrototype = Prototype.emptyFunction; + if (reverse) { + for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) { + var node = nodes[i]; + if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++; + } + } else { + for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++) + if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++; + } + }, + + // filters out duplicates and extends all nodes + unique: function(nodes) { + if (nodes.length == 0) return nodes; + var results = [], n; + for (var i = 0, l = nodes.length; i < l; i++) + // use `typeof` operator to prevent errors + if (typeof (n = nodes[i])._countedByPrototype == 'undefined') { + n._countedByPrototype = Prototype.emptyFunction; + results.push(n); + } + return Prototype.LegacySelector.handlers.unmark(results); + }, + + // COMBINATOR FUNCTIONS + descendant: function(nodes) { + var h = Prototype.LegacySelector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + h.concat(results, node.getElementsByTagName('*')); + return results; + }, + + child: function(nodes) { + var h = Prototype.LegacySelector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) { + for (var j = 0, child; child = node.childNodes[j]; j++) + if (child.nodeType == 1 && child.tagName != '!') results.push(child); + } + return results; + }, + + adjacent: function(nodes) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + var next = this.nextElementSibling(node); + if (next) results.push(next); + } + return results; + }, + + laterSibling: function(nodes) { + var h = Prototype.LegacySelector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + h.concat(results, Element.nextSiblings(node)); + return results; + }, + + nextElementSibling: function(node) { + while (node = node.nextSibling) + if (node.nodeType == 1) return node; + return null; + }, + + previousElementSibling: function(node) { + while (node = node.previousSibling) + if (node.nodeType == 1) return node; + return null; + }, + + // TOKEN FUNCTIONS + tagName: function(nodes, root, tagName, combinator) { + var uTagName = tagName.toUpperCase(); + var results = [], h = Prototype.LegacySelector.handlers; + if (nodes) { + if (combinator) { + // fastlane for ordinary descendant combinators + if (combinator == "descendant") { + for (var i = 0, node; node = nodes[i]; i++) + h.concat(results, node.getElementsByTagName(tagName)); + return results; + } else nodes = this[combinator](nodes); + if (tagName == "*") return nodes; + } + for (var i = 0, node; node = nodes[i]; i++) + if (node.tagName.toUpperCase() === uTagName) results.push(node); + return results; + } else return root.getElementsByTagName(tagName); + }, + + id: function(nodes, root, id, combinator) { + var targetNode = $(id), h = Prototype.LegacySelector.handlers; + + if (root == document) { + // We don't have to deal with orphan nodes. Easy. + if (!targetNode) return []; + if (!nodes) return [targetNode]; + } else { + // In IE, we can check sourceIndex to see if root is attached + // to the document. If not (or if sourceIndex is not present), + // we do it the hard way. + if (!root.sourceIndex || root.sourceIndex < 1) { + var nodes = root.getElementsByTagName('*'); + for (var j = 0, node; node = nodes[j]; j++) { + if (node.id === id) return [node]; + } + } + } + + if (nodes) { + if (combinator) { + if (combinator == 'child') { + for (var i = 0, node; node = nodes[i]; i++) + if (targetNode.parentNode == node) return [targetNode]; + } else if (combinator == 'descendant') { + for (var i = 0, node; node = nodes[i]; i++) + if (Element.descendantOf(targetNode, node)) return [targetNode]; + } else if (combinator == 'adjacent') { + for (var i = 0, node; node = nodes[i]; i++) + if (Prototype.LegacySelector.handlers.previousElementSibling(targetNode) == node) + return [targetNode]; + } else nodes = h[combinator](nodes); + } + for (var i = 0, node; node = nodes[i]; i++) + if (node == targetNode) return [targetNode]; + return []; + } + return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : []; + }, + + className: function(nodes, root, className, combinator) { + if (nodes && combinator) nodes = this[combinator](nodes); + return Prototype.LegacySelector.handlers.byClassName(nodes, root, className); + }, + + byClassName: function(nodes, root, className) { + if (!nodes) nodes = Prototype.LegacySelector.handlers.descendant([root]); + var needle = ' ' + className + ' '; + for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) { + nodeClassName = node.className; + if (nodeClassName.length == 0) continue; + if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle)) + results.push(node); + } + return results; + }, + + attrPresence: function(nodes, root, attr, combinator) { + if (!nodes) nodes = root.getElementsByTagName("*"); + if (nodes && combinator) nodes = this[combinator](nodes); + var results = []; + for (var i = 0, node; node = nodes[i]; i++) + if (Element.hasAttribute(node, attr)) results.push(node); + return results; + }, + + attr: function(nodes, root, attr, value, operator, combinator) { + if (!nodes) nodes = root.getElementsByTagName("*"); + if (nodes && combinator) nodes = this[combinator](nodes); + var handler = Prototype.LegacySelector.operators[operator], results = []; + for (var i = 0, node; node = nodes[i]; i++) { + var nodeValue = Element.readAttribute(node, attr); + if (nodeValue === null) continue; + if (handler(nodeValue, value)) results.push(node); + } + return results; + }, + + pseudo: function(nodes, name, value, root, combinator) { + if (nodes && combinator) nodes = this[combinator](nodes); + if (!nodes) nodes = root.getElementsByTagName("*"); + return Prototype.LegacySelector.pseudos[name](nodes, value, root); + } + }, + + pseudos: { + 'first-child': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + if (Prototype.LegacySelector.handlers.previousElementSibling(node)) continue; + results.push(node); + } + return results; + }, + 'last-child': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + if (Prototype.LegacySelector.handlers.nextElementSibling(node)) continue; + results.push(node); + } + return results; + }, + 'only-child': function(nodes, value, root) { + var h = Prototype.LegacySelector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!h.previousElementSibling(node) && !h.nextElementSibling(node)) + results.push(node); + return results; + }, + 'nth-child': function(nodes, formula, root) { + return Prototype.LegacySelector.pseudos.nth(nodes, formula, root); + }, + 'nth-last-child': function(nodes, formula, root) { + return Prototype.LegacySelector.pseudos.nth(nodes, formula, root, true); + }, + 'nth-of-type': function(nodes, formula, root) { + return Prototype.LegacySelector.pseudos.nth(nodes, formula, root, false, true); + }, + 'nth-last-of-type': function(nodes, formula, root) { + return Prototype.LegacySelector.pseudos.nth(nodes, formula, root, true, true); + }, + 'first-of-type': function(nodes, formula, root) { + return Prototype.LegacySelector.pseudos.nth(nodes, "1", root, false, true); + }, + 'last-of-type': function(nodes, formula, root) { + return Prototype.LegacySelector.pseudos.nth(nodes, "1", root, true, true); + }, + 'only-of-type': function(nodes, formula, root) { + var p = Prototype.LegacySelector.pseudos; + return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root); + }, + + // handles the an+b logic + getIndices: function(a, b, total) { + if (a == 0) return b > 0 ? [b] : []; + return $R(1, total).inject([], function(memo, i) { + if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i); + return memo; + }); + }, + + // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type + nth: function(nodes, formula, root, reverse, ofType) { + if (nodes.length == 0) return []; + if (formula == 'even') formula = '2n+0'; + if (formula == 'odd') formula = '2n+1'; + var h = Prototype.LegacySelector.handlers, results = [], indexed = [], m; + h.mark(nodes); + for (var i = 0, node; node = nodes[i]; i++) { + if (!node.parentNode._countedByPrototype) { + h.index(node.parentNode, reverse, ofType); + indexed.push(node.parentNode); + } + } + if (formula.match(/^\d+$/)) { // just a number + formula = Number(formula); + for (var i = 0, node; node = nodes[i]; i++) + if (node.nodeIndex == formula) results.push(node); + } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b + if (m[1] == "-") m[1] = -1; + var a = m[1] ? Number(m[1]) : 1; + var b = m[2] ? Number(m[2]) : 0; + var indices = Prototype.LegacySelector.pseudos.getIndices(a, b, nodes.length); + for (var i = 0, node, l = indices.length; node = nodes[i]; i++) { + for (var j = 0; j < l; j++) + if (node.nodeIndex == indices[j]) results.push(node); + } + } + h.unmark(nodes); + h.unmark(indexed); + return results; + }, + + 'empty': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + // IE treats comments as element nodes + if (node.tagName == '!' || node.firstChild) continue; + results.push(node); + } + return results; + }, + + 'not': function(nodes, selector, root) { + var h = Prototype.LegacySelector.handlers, selectorType, m; + var exclusions = new Prototype.LegacySelector(selector).findElements(root); + h.mark(exclusions); + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!node._countedByPrototype) results.push(node); + h.unmark(exclusions); + return results; + }, + + 'enabled': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!node.disabled && (!node.type || node.type !== 'hidden')) + results.push(node); + return results; + }, + + 'disabled': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (node.disabled) results.push(node); + return results; + }, + + 'checked': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (node.checked) results.push(node); + return results; + } + }, + + operators: { + '=': function(nv, v) { return nv == v; }, + '!=': function(nv, v) { return nv != v; }, + '^=': function(nv, v) { return nv == v || nv && nv.startsWith(v); }, + '$=': function(nv, v) { return nv == v || nv && nv.endsWith(v); }, + '*=': function(nv, v) { return nv == v || nv && nv.include(v); }, + '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); }, + '|=': function(nv, v) { return ('-' + (nv || "").toUpperCase() + + '-').include('-' + (v || "").toUpperCase() + '-'); } + }, + + split: function(expression) { + var expressions = []; + expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) { + expressions.push(m[1].strip()); + }); + return expressions; + }, + + matchElements: function(elements, expression) { + var matches = $$(expression), h = Prototype.LegacySelector.handlers; + h.mark(matches); + for (var i = 0, results = [], element; element = elements[i]; i++) + if (element._countedByPrototype) results.push(element); + h.unmark(matches); + return results; + }, + + findElement: function(elements, expression, index) { + if (Object.isNumber(expression)) { + index = expression; expression = false; + } + return Prototype.LegacySelector.matchElements(elements, expression || '*')[index || 0]; + }, + + findChildElements: function(element, expressions) { + expressions = Prototype.LegacySelector.split(expressions.join(',')); + var results = [], h = Prototype.LegacySelector.handlers; + for (var i = 0, l = expressions.length, selector; i < l; i++) { + selector = new Prototype.LegacySelector(expressions[i].strip()); + h.concat(results, selector.findElements(element)); + } + return (l > 1) ? h.unique(results) : results; + } +}); diff --git a/vendor/legacy_selector/selector_engine.js b/vendor/legacy_selector/selector_engine.js new file mode 100644 index 000000000..3c5cf305f --- /dev/null +++ b/vendor/legacy_selector/selector_engine.js @@ -0,0 +1,15 @@ +//= require "repository/legacy_selector" + +;(function(engine) { + function select(selector, scope) { + return engine.findChildElements(scope || document, [selector]); + } + + function match(element, selector) { + return !!engine.findElement([element], selector); + } + + Prototype.Selector.engine = engine; + Prototype.Selector.select = select; + Prototype.Selector.match = match; +})(Prototype.LegacySelector); diff --git a/vendor/nwmatcher/repository b/vendor/nwmatcher/repository new file mode 160000 index 000000000..c9f5d5d4f --- /dev/null +++ b/vendor/nwmatcher/repository @@ -0,0 +1 @@ +Subproject commit c9f5d5d4fc4ca294477f803bb8d688a8d45de664 diff --git a/vendor/nwmatcher/selector_engine.js b/vendor/nwmatcher/selector_engine.js new file mode 100644 index 000000000..23af2afa8 --- /dev/null +++ b/vendor/nwmatcher/selector_engine.js @@ -0,0 +1,16 @@ +Prototype._original_property = window.NW; +//= require "repository/src/nwmatcher" + +;(function(engine) { + function select(selector, scope) { + return engine.select(selector, scope); + } + + Prototype.Selector.engine = engine; + Prototype.Selector.select = select; + Prototype.Selector.match = engine.match; +})(NW.Dom); + +// Restore globals. +window.NW = Prototype._original_property; +delete Prototype._original_property; diff --git a/vendor/pdoc b/vendor/pdoc index 147250bd6..b8da8a0e3 160000 --- a/vendor/pdoc +++ b/vendor/pdoc @@ -1 +1 @@ -Subproject commit 147250bd65eed627e32ca5a70b57fe4f7803ab4b +Subproject commit b8da8a0e3e33192bf74a0eecdcb88108bf9adcfd diff --git a/vendor/sizzle b/vendor/sizzle deleted file mode 160000 index e0f5cbc75..000000000 --- a/vendor/sizzle +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e0f5cbc75d12aa78f3ef30930414b2f88da7b2b8 diff --git a/vendor/sizzle/repository b/vendor/sizzle/repository new file mode 160000 index 000000000..a9eb3ca3c --- /dev/null +++ b/vendor/sizzle/repository @@ -0,0 +1 @@ +Subproject commit a9eb3ca3c5e1b568057390f73da385809ac69340 diff --git a/vendor/sizzle/selector_engine.js b/vendor/sizzle/selector_engine.js new file mode 100644 index 000000000..40ecfbb16 --- /dev/null +++ b/vendor/sizzle/selector_engine.js @@ -0,0 +1,59 @@ +Prototype._original_property = window.Sizzle; + +;(function () { + function fakeDefine(fn) { + Prototype._actual_sizzle = fn(); + } + fakeDefine.amd = true; + + if (typeof define !== 'undefined' && define.amd) { + // RequireJS is loaded. We need to pretend to be `define` while Sizzle + // runs. + Prototype._original_define = define; + Prototype._actual_sizzle = null; + window.define = fakeDefine; + } +})(); + +//= require "repository/dist/sizzle" + +;(function() { + if (typeof Sizzle !== 'undefined') { + // Sizzle was properly defined. + return; + } + + if (typeof define !== 'undefined' && define.amd) { + // RequireJS. + // We should find Sizzle where we put it. And we need to restore the original `define`. + window.Sizzle = Prototype._actual_sizzle; + window.define = Prototype._original_define; + delete Prototype._actual_sizzle; + delete Prototype._original_define; + // TODO: Should we make our own `define` call here? + } else if (typeof module !== 'undefined' && module.exports) { + // Sizzle saw that it's in a CommonJS environment and attached itself to + // `module.exports` instead. + window.Sizzle = module.exports; + // Reset `module.exports`. + module.exports = {}; + } +})(); + +;(function(engine) { + function select(selector, scope) { + return engine(selector, scope || document); + } + + function match(element, selector) { + return engine.matches(selector, [element]).length == 1; + } + + Prototype.Selector.engine = engine; + Prototype.Selector.select = select; + Prototype.Selector.match = match; +})(window.Sizzle); + +// Restore globals. +window.Sizzle = Prototype._original_property; +delete Prototype._original_property; diff --git a/vendor/slick/repository b/vendor/slick/repository new file mode 160000 index 000000000..b7d3a3305 --- /dev/null +++ b/vendor/slick/repository @@ -0,0 +1 @@ +Subproject commit b7d3a3305675cfaa326c55b3eb1d4c4d4f962744 diff --git a/vendor/slick/selector_engine.js b/vendor/slick/selector_engine.js new file mode 100644 index 000000000..11e5ca1b8 --- /dev/null +++ b/vendor/slick/selector_engine.js @@ -0,0 +1,17 @@ +Prototype._original_property = window.Slick; +//= require "repository/Source/Slick.Parser.js" +//= require "repository/Source/Slick.Finder.js" + +;(function(engine) { + function select(selector, scope) { + return engine.search(scope || document, selector); + } + + Prototype.Selector.engine = engine; + Prototype.Selector.select = select; + Prototype.Selector.match = engine.match; +})(Slick); + +// Restore globals. +window.Slick = Prototype._original_property; +delete Prototype._original_property; diff --git a/vendor/unittest_js b/vendor/unittest_js deleted file mode 160000 index b81172275..000000000 --- a/vendor/unittest_js +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b81172275ea4a6530a5bac4912f2f18ecbfe9feb