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 26e3784ea..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,3 +8,13 @@
path = vendor/sprockets
url = git://github.com/sstephenson/sprockets.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 2d4e5953c..796e90876 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,110 @@
+-----------------------------------------------------------------------------
+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)
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 60889c51f..a0a795150 100755
--- a/Rakefile
+++ b/Rakefile
@@ -1,8 +1,11 @@
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')
@@ -13,62 +16,215 @@ module PrototypeHelper
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']
-
- def self.sprocketize(path, source, destination = nil, strip_comments = true)
+
+ 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
+
+ def self.has_git?
+ begin
+ `git --version`
+ return true
+ 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://git-scm.com/book/en/v2/Getting-Started-Installing-Git"
+ exit
+ end
+
+ def self.sprocketize(options = {})
+ options = {
+ :destination => File.join(DIST_DIR, options[:source]),
+ :strip_comments => true
+ }.merge(options)
+
require_sprockets
+ 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],
- :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 = <
+
+
+
+
+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.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
- missing_file = e.message.sub('no such file to load -- ', '')
- if missing_file == path
- puts "\nIt looks like you're missing #{name}. Just run:\n\n"
- puts " $ git submodule init"
- puts " $ git submodule update vendor/#{path}"
- puts "\nand you should be all set.\n\n"
+ # 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 -- ', '').sub('cannot load such file -- ', '')
+ if missing_file == full_path
+ # Missing a git submodule.
+ retry if get_submodule(name, path)
else
+ # Missing a gem.
puts "\nIt looks like #{name} is missing the '#{missing_file}' gem. Just run:\n\n"
puts " $ gem install #{missing_file}"
puts "\nand you should be all set.\n\n"
@@ -76,25 +232,29 @@ module PrototypeHelper
exit
end
end
-end
-%w[sprockets pdoc unittest_js caja_builder].each do |name|
- $:.unshift File.join(PrototypeHelper::ROOT_DIR, 'vendor', name, 'lib')
+ 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
@@ -104,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|
@@ -123,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 4ecb83f6e..000000000
--- a/src/dom/dom.js
+++ /dev/null
@@ -1,2397 +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
- *
- * The `Element` object provides a variety of powerful DOM methods for
- * interacting with DOM elements — creating them, updating them,
- * traversing them, etc. You can access these either as methods of `Element`
- * itself, passing in the element to work with as the first argument, or as
- * methods on extended element *instances*:
- *
- * // Using Element:
- * Element.addClassName('target', 'highlighted');
- *
- * // Using an extended element instance:
- * $('target').addClassName('highlighted');
- *
- * `Element` is also a constructor for building element instances from scratch,
- * see [`new Element`](#new-constructor) for details.
- *
- * Most `Element` methods return the element instance, so that you can chain
- * them easily:
- *
- * $('message').addClassName('read').update('I read this message!');
- *
- * ##### More Information
- *
- * For more information about extended elements, check out ["How Prototype
- * extends the DOM"](http://prototypejs.org/learn/extensions), which will walk
- * you through the inner workings of Prototype's DOM extension mechanism.
-**/
-
-/**
- * new Element(tagName[, attributes])
- * - tagName (String): The name of the HTML element to create.
- * - attributes (Object): An optional group of attribute/value pairs to set on
- * the element.
- *
- * Creates an HTML element with `tagName` as the tag name, optionally with the
- * given attributes. This can be markedly more concise than working directly
- * with the DOM methods, and takes advantage of Prototype's workarounds for
- * various browser issues with certain attributes:
- *
- * ##### Example
- *
- * // The old way:
- * var a = document.createElement('a');
- * a.setAttribute('class', 'foo');
- * a.setAttribute('href', '/foo.html');
- * a.appendChild(document.createTextNode("Next page"));
- *
- * // The new way:
- * var a = new Element('a', {'class': 'foo', href: '/foo.html'}).update("Next page");
-**/
-(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', '
', 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
+ *
+ *
+ *
+**/
+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
+ *
+ *
+ *
+ *
+ * Then:
+ *
+ * $('framer').makeClipping().setStyle({width: '100px', height: '100px'});
+ * // -> Element
+ *
+ * Another example:
+ *
+ * language: html
+ * Click me to try it out.
+ *
+ *
+ *
+ *
+ *
+ *
+ **/
+ 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
+ *
+ *
+ *
+ *
+ * Then:
+ *
+ * $('framer').undoClipping();
+ * // -> Element (and sets the CSS overflow property to its original value).
+ *
+ * Another example:
+ *
+ * language: html
+ * Click me to try it out.
+ *
+ *
+ *
+ *
+ *
+ *
+ **/
+ 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
+ *
+ *
+ *
+ *
+ *
Name
+ *
Default
+ *
Description
+ *
+ *
+ *
+ *
+ *
setLeft
+ *
true
+ *
Clones source's left CSS property onto element.
+ *
+ *
+ *
setTop
+ *
true
+ *
Clones source's top CSS property onto element.
+ *
+ *
+ *
setWidth
+ *
true
+ *
Clones source's width onto element.
+ *
+ *
+ *
setHeight
+ *
true
+ *
Clones source's width onto element.
+ *
+ *
+ *
offsetLeft
+ *
0
+ *
Number by which to offset element's left CSS property.
+ *
+ *
+ *
offsetTop
+ *
0
+ *
Number 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
*
* // 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.
*
- *
- *
- * ['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.
*
- *
+ * ##### 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 = ' ';
+ *
+ * markdown.gsub(/!\[(.*?)\]\((.*?)\)/, function(match) {
+ * return '';
+ * });
+ * // -> ''
+ *
+ * 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(/!\[(.*?)\]\((.*?)\)/, '');
+ * // -> ''
+ *
+ * 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 = ' ';
+ *
+ * markdown.sub(/!\[(.*?)\]\((.*?)\)/, function(match) {
+ * return '';
+ * });
+ * // -> ' '
+ *
+ * markdown.sub(/!\[(.*?)\]\((.*?)\)/, '');
+ * // -> ' '
+ *
+ * ##### 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 `
-
-
-
-