diff --git a/.gitignore b/.gitignore index ee3335f13..d214bf775 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ .DS_Store pkg -test/unit/tmp/* \ No newline at end of file +test/unit/tmp/* +test/unit/browsers.yml +doc +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 403bbfc96..796e90876 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,236 @@ +----------------------------------------------------------------------------- +NOTE: This changelog is no longer updated. Consult the commit history for a +list of changes from version to version. +----------------------------------------------------------------------------- + +*1.7* (November 16, 2010) + +* Ensure `Element#update` works with string content that includes a LINK tag in Internet Explorer. [#264 state:resolved] (Tobias H. Michaelsen, Andrew Dupont) + +* Treat a 304 HTTP status as a successful response. [#331 state:resolved] (Kenneth Kin Lum, Andrew Dupont) + +* Handle sparse arrays properly in `Array#_each` to match behavior with browsers' built-in `Array#forEach` (and ES5). [#790 state:resolved] (Andriy Tyurnikov, Yaffle, Andrew Dupont) + +* Make `Event.extend` work with legacy IE events in IE 9. (Andrew Dupont) + +* Stop appending `&_=` to the parameters for non-GET Ajax requests in Safari. We no longer support any version of Safari for which this is necessary. [#327 state:resolved] (John-David Dalton, Andrew Dupont) + +* Ensure `Form.focusFirstElement` doesn't raise an exception on forms with no fields. [#341 state:resolved] (achernin, Andrew Dupont) + +* Define a `relatedTarget` property on extended mouseenter/mouseleave events in IE's legacy event system. [#708 state:resolved] (Walter Smith, Tobie Langel, Andrew Dupont) + +* Fix odd behavior with `new Element('select')` in IE6-7. [#480 state:resolved] (Bruce Harris, kangax, Andrew Dupont) + +* Extend BUTTON elements with everything defined in Form.Element.Methods. Ensure BUTTON elements are traversed in Form.getElements and serialized in Form.serialize. (Luis Gomez, Samuel Lebeau, kangax, Andrew Dupont) + +* Ensure Object.isFunction returns `false` for RegExp objects. [#661 state:resolved] (James, kangax, Andrew Dupont) + +* Revert Opera-specific behavior for calling Element#getStyle with (left|right|top|bottom). [#268 state:resolved] (kangax, Andrew Dupont) + +* Fix issue with Event#isMiddleClick and #isRightClick on Safari and Chrome. [#731 state:resolved] (Arthur Schreiber) + +* Fix issue where an Ajax request in IE sometimes returns 1223 instead of 204 as the status code. [#129 state:resolved] (adevadeh, gordyt, Andrew Dupont) + +* Add Object.isDate. [#443 state:resolved] (Nesterenko Dmitry, kangax, Samuel Lebeau, Andrew Dupont) + +* Handle cases where `document` or `document.documentElement` is passed into Element#getOffsetParent. Fixes IE errors with many layout/positioning methods. [#90 state:resolved] (Padraig Kennedy, Andrew Dupont) + +* Wrap `element` in `$` for Element#cumulativeOffset, #viewportOffset, #positionedOffset, and #getOffsetParent. [#782 state:resolved] (Radoslav Stankov, Andrew Dupont) + +*1.7_rc3* (October 11, 2010) + +* Feature detect IE's legacy event system, since IE9 now supports DOM L2 Events. (Michael Sciacqua, kangax, Andrew Dupont) + +* Fix an issue in IE9 beta where the `value` attribute of an element would not get set. (Andrew Dupont) + +* Move all offset methods to layout.js, eliminating some code repetition. + +* Remove unit test for passing a DOM node into `Object.toJSON`. To continue to support this use case would require bypassing IE's native JSON implementation. (Tobie Langel, Thomas Fuchs, Samuel Lebeau, Andrew Dupont) + +* Ensure hidden absolutely-positioned elements are sized correctly when they have no explicit width set. [#1084 state:resolved] (Viktor Kojouharov, Andrew Dupont) + +* Change custom events implementation to use the `onlosecapture` event instead of the `onfilterchange` event for non-bubbling custom events in Internet Explorer. (John-David Dalton, Andrew Dupont) + +* Revert `Element.getHeight` and `Element.getWidth` to their previous behavior in order to ensure backward-compatibility. (Sam Stephenson, Andrew Dupont) + +* Fix accidental declaration of `purgeElement` as a global function. [#1089 state:resolved] (Viktor Kojouharov, Andrew Dupont) + +* Ensure `Element.Layout` gives accurate measurements for percentages on all elements. Fix inaccurate measurements on `position: fixed` elements with percentages. [#1040 state:resolved] (Dan Popescu, Riccardo De Agostini, Andrew Dupont) + +* Fix issue with retrieving border-box-(height|width) of hidden elements. [#1035 state:resolved] (Francois-Pierre Bouchard, Andrew Dupont) + +* Ensure we clean up after ourselves for all `width` and `height` computations in `Element.Layout`. (Sam Stephenson, Andrew Dupont) + +*1.7_rc2* (May 12, 2010) + +* Remove redefinition of `Element#cumulativeOffset` when `getBoundingClientRect` is present, as it seems to give inaccurate results. (Andrew Dupont) + +* Optimize element purging. (RStankov, Andrew Dupont) + +* Add `Element.purge` for cleaning up event listeners and element storage keys on elements that will be removed from the page. Make `Element.update` perform similar cleanup automatically. (Andrew Dupont, Tobie Langel) + +* The second argument to Event.Handler callbacks is the target element when no selector is present, or the matching element when a selector is present. Callbacks are always bound to the original element. (sam) + +* Fix issue where `Element.Layout#get` would fail to interpret negative pixel values. (Sebastien Gruhier, Andrew Dupont) + +* Fix bugs in layout.js. Add tests for `Element.Layout#toCSS`, `#toObject`, and `#toHash`. (RStankov, Andrew Dupont) + +* Add `Element.Layout#toObject` and `Element.Layout.toHash`. (Andrew Dupont) + +* Make `Element.Layout#toCSS` return camelized property names, as expected by `Element.setStyle`. [#1021 state:resolved] (njakobsen, Andrew Dupont) + +*1.7_rc1* (April 1, 2010) + +* Add an `Event.Handler` class, plus `Event.on` and `Element#on` methods, for simplified event delegation. (sam, Tobie Langel, Andrew Dupont) + +* Make `Element.Layout` properly interpret computed non-integer pixel values. (e.g., Firefox can report "12.5px" as a computed style value.) (henrymazza) + +* Fix deprecated Selector.matchElements. (Tobie Langel) + +* Make Object.keys ES5 compliant. (Tobie Langel) + +* Make Prototype's JSON implementation EcmaScript 5 compliant. [#453 state:resolved] (Tobie Langel) + +* Also detect embedded (UIWebView) mobile Safari. (Thomas Fuchs) + +* Avoid object creation and an unnecessary function call in `Class#addMethods`, when working around JScript DontEnum bug. Replace with feature test and a simple boolean check at runtime. (kangax) + +* Optimize Element#immediateDescendants. (kangax, Tobie Langel) + +* Remove unnecessary function object creation and `Number#times` in `Element._getContentFromAnonymousElement`. (kangax) + +* Eliminate runtime forking and long method lookup in `Element.hasAttribute`. (kangax) + +* Remove redundant ternary. (kangax) + +* Avoid repeating declaration statements where it makes sense, for slightly better runtime performance and minification. (kangax) + +* Make `Event.stopObserving` return element in all cases. [#810 state:resolved] (Yaffle, Tobie Langel) + +* String#startsWith, String#endsWith performance optimization (Yaffle, Tobie Langel, kangax) + +* Rewrite String#camelize using String#replace with a replacement function (Phred, John-David Dalton, Samuel Lebeau, kangax) + +*1.6.1* (August 24, 2009) + +* Avoid triggering a warning when Java is disabled in IE8. [#668 state:resolved] (orv, kangax, Andrew Dupont, Tobie Langel) + +* Simplify String#(un)escapeHTML and remove their DOM dependencies. (Tobie Langel) + +* Update UnittestJS. Modifiy test template accordingly. (Tobie Langel) + +* Remove redundant if statement in Element#readAttribute. (Tobie Langel) + +* Add missing semicolons. [#751 state:resolved] (Diego Perini) + +* Remove expensive (for such low-level method) internal `getClass` in favor of plain string comparison (kangax) + +* Fix `PeriodicalExecuter` so that it no longer suppresses exceptions. [#696 state:resolved] (Samuel Lebeau, Yaffle) + +* Fix issue related to escaping of selectors for querySelectorAll. [#559 state:resolved] (Jorn Holm) + +*1.6.1_rc3* (June 16, 2009) + +* Fixed a variety of non-ASCII chars and similar [#610 state:resolved] (T.J. Crowder) + +* Add Chrome 1+ to the list of supported browsers. (kangax) + +* Fix `Template#evaluate` "eating" previous character if `null` was returned from `toTemplateReplacements` function. (Nir, Jürgen Hörmann, kangax) + +* Make sure (deficient) APPLET, OBJECT and EMBED elements are extended with simulated methods in IE8. Return early if `_extendedByPrototype` is present on an element. (Tobie Langel, kangax) + +* Replace array creation and `Array#include` with a more efficient `RegExp#test`. (kangax) + +* Reorganize the way `ElementExtensions` are defined. Make sure elements used in SpecificElementExtensions are cleaned up. (kangax) + +* Make sure $A works with primitive values. (mr_justin, kangax) + +* Do not browser sniff when forking `unmark` function in selector suite. Instead use a proper test - PROPERTIES_ATTRIBUTES_MAP. (kangax) + +* Do not use short-hand element methods notation (@element.getStyle() -> Element.getStyle(@element)) for performance reasons. Do not use `$A` and `Array.prototype.shift` when `Array.prototype.slice` can be used instead. (kangax) + +* `Prototype.Browser.Opera` now uses stronger inference and is determined by [[Class]] of `window.opera` being - "Opera". (kangax) + +* Fix error in event.js which prevented attaching more than one responder for an event name/element combination. [#651 state:resolved] (Rob Lineweaver) + +* Do not sniff when testing for IE's proprietary mouseenter/mouseleave events support. Use more robust inference instead. (kangax) + +* Use `Prototype.emptyFunction` consistently throughout unit tests. [#253 state:resolved] (Michael M Slusarz, John David Dalton, kangax) + +* deprecation extension: mark Array#reduce() as removed. [#569 state:resolved] (Tobie Langel) + +* `Form.serialize` now works safely with forms that have "length"-named elements. [#77 state:resolved] (Peter Adrianov, John-David Dalton, kangax) + +*1.6.1_rc1* (March 22, 2009) + +* `Element#update` now takes care of SCRIPT elements in IE. [#573 state:resolved] (Martin, Tobie Langel, kangax) + +* Remove unused local variables from `Element.extend`. Fix one of the form tests to remove `_extendedByPrototype` by setting it to `undefined` rather than `false` (`_extendedByPrototype` being `false` does not force `Element.extend` to re-extend element). (T.J. Crowder, kangax) + +* Make test for `escapeHTML`/`unescapeHTML` more strict. (Chrome 1.x escapes "<" and "&" with `innerHTML`, but not ">") (kangax) + +* Remove another sniffing from one of DOM tests. Fixes last IE8 failure. (kangax) + +* `Element.extend` now takes care of IE8 bug when HTMLAppletElement and HTMLObjectElement objects do not inherit from `Element.prototype`. (kangax) + +* Fix DOM tests to use proper feature test when testing `setOpacity` (kangax) + +* Fix another failure in IE8, `for`/`htmlFor` {get/set}Attribute translation. (kangax) + +* Fix `Element#writeAttribute` and `Element#readAttribute` failures in IE8 due to lack of proper feature testing. (kangax) + +* Remove sniffing from one of the DOM tests, which produced failures in IE8. (kangax) + +* Fix `Form.reset` test where `respondsTo` wouldn't detect a method due to typeof returning "object" (rather than "function") in IE (kangax) + +* Remove Array#reduce which currently overrides native `reduce` in clients implementing JS1.8, e.g. Firefox 3+ (Tobie Langel, Andrew Dupont, kangax) + +* Make sure try/catch/finally is used instead of try/finally for clients without support for the latter one (e.g. Blackberry, IE) (Ville Koskinen, kangax) + +* Use `in` operator when accessing property of a nodelist to prevent Safari <=2.0.4 from crashing (kangax) + +* Add Element#clone as a safe wrapper of native `cloneNode`. (Andrew Dupont, kangax) + +* Add tests to ensure IE8 properly assigns a class name in the `Element` constructor. [#529 state:resolved] (Riki Fridrich, Andrew Dupont) + +* Remove sniffing from `Element` when detecting broken `setAttribute` in IE. [#571 state:resolved] (kangax) + +* Remove sniffing from `Element.update` branching in favor of feature detection. [#574 state:resolved] (kangax) + +* Remove sniffing when branching `escapeHTML` and `unescapeHTML`. [#570 state:resolved] (kangax) + +* Redefine Element#down in IE 6-7 to avoid extending all descendants when no selector is given. [#452 state:resolved] (eno, Andrew Dupont) + +* Reverse the definitions of Event#pointer(X|Y) and Event#pointer to prevent unnecessary computation. [#403 state:resolved] (Nick Stakenburg, Andrew Dupont) + +* Add first-class support for `mouseenter` and `mouseleave` events in non-IE browsers (IE supports them natively). [#350 state:resolved] (Nick Stakenburg, Andrew Dupont) + +* Make sure `_extendedByPrototype`, `_countedByPrototype`, and `prototypeUID` node expandos are accessed with `typeof` to prevent errors in some environments. [#354 state:resolved] (Hilberty, kangax, Andrew Dupont) + +* Fix issue where Opera 9.x returns incorrect results on certain Selector queries with descendant combinators. [#395 state:resolved] (Arpan, fearphage, kangax, Andrew Dupont) + +* Null out references to elements in cache on page unload. Need this in addition to the Event#stopObserving calls to clean up memory leaks. [#425 state:resolved] (ykphuah, mr_justin, Andrew Dupont) + +* Ensure `toString` and `valueOf` properties are copied to a subclass only when necessary in IE6. [#382 state:resolved] (Samuel Lebeau) + +* Make sure `getAttribute` is used without flag when accessing the "type" attribute of an iframe (IE throws error otherwise). [#118 state:resolved] (Zekid, kangax) + +* String#gsub should escape RegExp metacharacters when the first argument is a string. [#469 state:resolved] (michael, kangax) + +* Fix order of replacement in String#unescapeHTML [#544 state:resolved] (SWeini, kangax) + +* Fix issue where a Selector query rooted on a node that had not been attached to the document failed in IE. [#464 state:resolved] (jddalton, kangax, Douglas Fraser, Andrew Dupont) + +* Fix Selector to match elements with attributes containing hyphens. [#285 state:resolved] (leiyou, jddalton, kangax) + +* Make sure Form.reset always returns a reference to the receiver element. [#309 state:resolved] (Phil, kangax) + +* Escape ":" and "." characters when doing contextual CSS selection in browsers that support querySelectorAll. [#559 state:resolved] (fxtentacle, Andrew Dupont) + +* Ensure the `target` property on events is never undefined in IE. [#383 state:resolved] (Mathias Karstädt, Diego Perini, Andrew Dupont) + +* Ensure Element#descendants always returns an array. [#373 state:resolved] (kangax) + * Don't switch fixed position elements to absolute in Element.getDimensions [#543 state:resolved] (James Wheare) * Avoid infinite loops when calling String#sub with empty pattern [#534 state:resolved] (Samuel Lebeau) 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 new file mode 100644 index 000000000..afc7d0fdc --- /dev/null +++ b/README.markdown @@ -0,0 +1,63 @@ +Prototype +========= + +#### An object-oriented JavaScript framework #### + +Prototype is a JavaScript framework that aims to ease development of dynamic +web applications. It offers a familiar class-style OO framework, extensive +Ajax support, higher-order programming constructs, and easy DOM manipulation. + +### Targeted platforms ### + +Prototype currently targets the following platforms: + +* Microsoft Internet Explorer for Windows, version 6.0 and higher +* Mozilla Firefox 1.5 and higher +* Apple Safari 2.0.4 and higher +* Opera 9.25 and higher +* Chrome 1.0 and higher + +Using Prototype +--------------- + +To use Prototype in your application, download the latest release from the +Prototype web site () and copy +`dist/prototype.js` to a suitable location. Then include it in your HTML +like so: + + + +### Building Prototype from source ### + +`prototype.js` is a composite file generated from many source files in +the `src/` directory. To build Prototype, you'll need: + +* a copy of the Prototype source tree, either from a distribution tarball or + from the Git repository (see below) +* Ruby 1.8.2 or higher () +* Rake--Ruby Make () +* RDoc, if your Ruby distribution does not include it + +From the root Prototype directory: + +* `rake dist` will preprocess the Prototype source using Sprockets and + generate the composite `dist/prototype.js` +* `rake package` will create a distribution tarball in the + `pkg/` directory + +Contributing to Prototype +------------------------- + +Check out the Prototype source with + + $ git clone git://github.com/sstephenson/prototype.git + $ cd prototype + $ git submodule init + $ git submodule update vendor/sprockets vendor/pdoc vendor/unittest_js + +Find out how to contribute: . + +Documentation +------------- + +Please see the online Prototype API: . diff --git a/README.rdoc b/README.rdoc deleted file mode 100644 index 396aca2c8..000000000 --- a/README.rdoc +++ /dev/null @@ -1,59 +0,0 @@ -= Prototype -==== An object-oriented JavaScript framework - -Prototype is a JavaScript framework that aims to ease development of dynamic -web applications. It offers a familiar class-style OO framework, extensive -Ajax support, higher-order programming constructs, and easy DOM manipulation. - -=== Targeted platforms - -Prototype currently targets the following platforms: - -* Microsoft Internet Explorer for Windows, version 6.0 and higher -* Mozilla Firefox 1.5 and higher -* Apple Safari 2.0 and higher -* Opera 9.25 and higher - -== Using Prototype - -To use Prototype in your application, download the latest release from the -Prototype web site (http://prototypejs.org/download) and copy -dist/prototype.js to a suitable location. Then include it in your HTML -like so: - - - -=== Building Prototype from source - -prototype.js is a composite file generated from many source files in -the src/ directory. To build Prototype, you'll need: - -* a copy of the Prototype source tree, either from a distribution tarball or - from the Git repository (see below) -* Ruby 1.8.2 or higher (http://www.ruby-lang.org/) -* Rake -- Ruby Make (http://rake.rubyforge.org/) -* RDoc, if your Ruby distribution does not include it - -From the root Prototype directory, - -* rake dist will preprocess the Prototype source using ERB and - generate the composite dist/prototype.js. -* rake package will create a distribution tarball in the - pkg/ directory. - -== Contributing to Prototype - -Check out the Prototype source with - $ git clone git://github.com/sstephenson/prototype.git - $ cd prototype - $ git submodule init - $ git submodule update - -Find out how to contribute: - http://prototypejs.org/contribute - -== Documentation - -Please see the online Prototype API: - http://prototypejs.org/api - diff --git a/Rakefile b/Rakefile index a1c87d2bd..a0a795150 100755 --- a/Rakefile +++ b/Rakefile @@ -1,68 +1,262 @@ require 'rake' require 'rake/packagetask' +require 'rbconfig' +require 'yaml' -PROTOTYPE_ROOT = File.expand_path(File.dirname(__FILE__)) -PROTOTYPE_SRC_DIR = File.join(PROTOTYPE_ROOT, 'src') -PROTOTYPE_DIST_DIR = File.join(PROTOTYPE_ROOT, 'dist') -PROTOTYPE_DOC_DIR = File.join(PROTOTYPE_ROOT, 'doc') -PROTOTYPE_PKG_DIR = File.join(PROTOTYPE_ROOT, 'pkg') -PROTOTYPE_TEST_DIR = File.join(PROTOTYPE_ROOT, 'test') -PROTOTYPE_TEST_UNIT_DIR = File.join(PROTOTYPE_TEST_DIR, 'unit') -PROTOTYPE_TMP_DIR = File.join(PROTOTYPE_TEST_UNIT_DIR, 'tmp') -PROTOTYPE_VERSION = YAML.load(IO.read(File.join(PROTOTYPE_SRC_DIR, 'constants.yml')))['PROTOTYPE_VERSION'] +module PrototypeHelper + extend Rake::DSL -$:.unshift File.join(PROTOTYPE_ROOT, 'vendor', 'sprockets', 'lib') + ROOT_DIR = File.expand_path(File.dirname(__FILE__)) + SRC_DIR = File.join(ROOT_DIR, 'src') + DIST_DIR = File.join(ROOT_DIR, 'dist') + DOC_DIR = File.join(ROOT_DIR, 'doc') + TEMPLATES_DIR = File.join(ROOT_DIR, 'templates') + PKG_DIR = File.join(ROOT_DIR, 'pkg') + TEST_DIR = File.join(ROOT_DIR, 'test') + TEST_UNIT_DIR = File.join(TEST_DIR, 'unit') + TMP_DIR = File.join(TEST_UNIT_DIR, 'tmp') + VERSION = YAML.load(IO.read(File.join(SRC_DIR, 'constants.yml')))['PROTOTYPE_VERSION'] -task :default => [:dist, :dist_helper, :package, :clean_package_source] + 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, options[:path]), + :load_path => load_path, + :source_files => [options[:source]], + :strip_comments => options[:strip_comments] + ) + + secretary.concatenation.save_to(options[:destination]) + end + + def self.build_doc_for(file) + rm_rf(DOC_DIR) + mkdir_p(DOC_DIR) + hash = current_head + index_header = < + + Prototype JavaScript Framework API + + +EOF + PDoc.run({ + :source_files => Dir[File.join('src', 'prototype', '**', '*.js')], + :destination => DOC_DIR, + :index_page => 'README.markdown', + :syntax_highlighter => syntax_highlighter, + :markdown_parser => :bluecloth, + :src_code_text => "View source on GitHub →", + :src_code_href => proc { |obj| + "https://github.com/sstephenson/prototype/blob/#{hash}/#{obj.file}#L#{obj.line_number}" + }, + :pretty_urls => false, + :bust_cache => false, + :name => 'Prototype JavaScript Framework', + :short_name => 'Prototype', + :home_url => 'http://prototypejs.org', + :version => PrototypeHelper::VERSION, + :index_header => index_header, + :footer => 'This work is licensed under a Creative Commons Attribution-Share Alike 3.0 Unported License.', + :assets => 'doc_assets' + }) + end + + def self.require_package(name) + begin + require name + rescue LoadError + puts "You need the #{name} package. Try installing it with:\n" + puts " $ gem install #{name}" + exit + end + end + + def self.require_phantomjs + cmd = IS_WINDOWS ? "phantomjs.cmd -v" : "phantomjs -v > /dev/null 2>&1" + success = system(cmd) + if !success + puts "\nYou need phantomjs installed to run this task. Find out how at:" + puts " http://phantomjs.org/download.html" + exit + end + end + + def self.syntax_highlighter + if ENV['SYNTAX_HIGHLIGHTER'] + highlighter = ENV['SYNTAX_HIGHLIGHTER'].to_sym + require_highlighter(highlighter, true) + return highlighter + end + + SYNTAX_HIGHLIGHTERS.detect { |n| require_highlighter(n) } + end + + def self.require_highlighter(name, verbose=false) + case name + when :pygments + success = system("pygmentize -V > /dev/null") + if !success && verbose + puts "\nYou asked to use Pygments, but I can't find the 'pygmentize' binary." + puts "To install, visit:\n" + puts " http://pygments.org/docs/installation/\n\n" + exit + end + return success # (we have pygments) + when :coderay + begin + require 'coderay' + rescue LoadError + if verbose + puts "\nYou asked to use CodeRay, but I can't find the 'coderay' gem. Just run:\n\n" + puts " $ gem install coderay" + puts "\nand you should be all set.\n\n" + exit + end + return false + end + return true # (we have CodeRay) + when :none + return true + else + puts "\nYou asked to use a syntax highlighter I don't recognize." + puts "Valid options: #{SYNTAX_HIGHLIGHTERS.join(', ')}\n\n" + exit + end + end + + def self.require_sprockets + require_submodule('Sprockets', 'sprockets') + end + + def self.require_pdoc + require_submodule('PDoc', 'pdoc') + end + + def self.require_unittest_js + require_submodule('UnittestJS', 'unittest_js') + end + + def self.require_caja_builder + require_submodule('CajaBuilder', 'caja_builder') + end + + def self.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" -def sprocketize(path, source, destination = source) - begin - require "sprockets" - rescue LoadError => e - puts "\nYou'll need Sprockets to build Prototype. Just run:\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" - puts "\nand you should be all set.\n\n" - end - - secretary = Sprockets::Secretary.new( - :root => File.join(PROTOTYPE_ROOT, path), - :load_path => [PROTOTYPE_SRC_DIR], - :source_files => [source] - ) - - secretary.concatenation.save_to(File.join(PROTOTYPE_DIST_DIR, destination)) + puts " $ git submodule update vendor/#{path}" + false + end + + def self.require_submodule(name, path) + begin + full_path = File.join(PrototypeHelper::ROOT_DIR, 'vendor', path, 'lib', path) + # We need to require the explicit version in the submodule. + require full_path + rescue LoadError => e + # Wait until we notice that a submodule is missing before we bother the + # user about installing git. (Maybe they brought all the files over + # from a different machine.) + missing_file = e.message.sub('no such file to load -- ', '').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" + end + exit + end + end + + def self.current_head + `git show-ref --hash HEAD`.chomp[0..6] + end end +task :default => [:dist, :dist_helper, :package, :clean_package_source] + desc "Builds the distribution." task :dist do - 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 - require 'protodoc' - require 'tempfile' - - Tempfile.open("prototype-doc") do |temp| - source = File.join(PROTOTYPE_SRC_DIR, 'prototype.js') - temp << Protodoc::Preprocessor.new(source, :strip_documentation => false) - temp.flush - rm_rf PROTOTYPE_DOC_DIR - PDoc::Runner.new(temp.path, :output => PROTOTYPE_DOC_DIR).run - end - end - + PrototypeHelper.build_doc_for(ENV['SECTION'] ? "#{ENV['SECTION']}.js" : 'prototype.js') + end + task :require do - lib = 'vendor/pdoc/lib/pdoc' - unless File.exists?(lib) - puts "\nYou'll need PDoc to generate the documentation. Just run:\n\n" - puts " $ git submodule init" - puts " $ git submodule update" - puts "\nand you should be all set.\n\n" - end - require lib + PrototypeHelper.require_pdoc end end @@ -70,12 +264,12 @@ task :doc => ['doc:build'] desc "Builds the updating helper." task :dist_helper do - sprocketize("ext/update_helper", "prototype_update_helper.js") + PrototypeHelper.sprocketize(:path => 'ext/update_helper', :source => 'prototype_update_helper.js') end -Rake::PackageTask.new('prototype', PROTOTYPE_VERSION) do |package| +Rake::PackageTask.new('prototype', PrototypeHelper::VERSION) do |package| package.need_tar_gz = true - package.package_dir = PROTOTYPE_PKG_DIR + package.package_dir = PrototypeHelper::PKG_DIR package.package_files.include( '[A-Z]*', 'dist/prototype.js', @@ -86,100 +280,40 @@ Rake::PackageTask.new('prototype', PROTOTYPE_VERSION) do |package| end task :clean_package_source do - rm_rf File.join(PROTOTYPE_PKG_DIR, "prototype-#{PROTOTYPE_VERSION}") + 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 => PROTOTYPE_TMP_DIR) - - Dir[File.join(PROTOTYPE_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 => PROTOTYPE_TEST_UNIT_DIR, - :assets_dir => PROTOTYPE_DIST_DIR - }) - selected_tests = (ENV['TESTS'] || '').split(',') - builder.collect(*selected_tests) - builder.render - end - - task :clean => [:require] do - UnittestJS::Builder.empty_dir!(PROTOTYPE_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 - lib = 'vendor/unittest_js/lib/unittest_js' - unless File.exists?(lib) - puts "\nYou'll need UnittestJS to run the tests. Just run:\n\n" - puts " $ git submodule init" - puts " $ git submodule update" - puts "\nand you should be all set.\n\n" - end - require lib + 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 => PROTOTYPE_TEST_UNIT_DIR, - :assets_dir => PROTOTYPE_DIST_DIR, - :whitelist_dir => File.join(PROTOTYPE_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 - lib = 'vendor/caja_builder/lib/caja_builder' - unless File.exists?(lib) - puts "\nYou'll need UnittestJS to run the tests. Just run:\n\n" - puts " $ git submodule init" - puts " $ git submodule update" - puts "\nand you should be all set.\n\n" - end - require lib + + 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/doc_assets/images/header-logo-small.png b/doc_assets/images/header-logo-small.png new file mode 100644 index 000000000..f377977fb Binary files /dev/null and b/doc_assets/images/header-logo-small.png differ diff --git a/doc_assets/images/header-stripe-small.png b/doc_assets/images/header-stripe-small.png new file mode 100644 index 000000000..e10464063 Binary files /dev/null and b/doc_assets/images/header-stripe-small.png differ diff --git a/ext/update_helper/prototype_update_helper.html b/ext/update_helper/prototype_update_helper.html index d2d432521..625911d5d 100644 --- a/ext/update_helper/prototype_update_helper.html +++ b/ext/update_helper/prototype_update_helper.html @@ -6,8 +6,8 @@ - - + +

Prototype Unit test file

@@ -45,8 +45,8 @@

Prototype Unit test file

var actualType = log.type.beforeLast() || log.type.last(); this.assertEqual(expected, actualMessage, 'assertInfoNotified'); this.assertEqual('info', actualType, 'assertInfoNotified'); - log.message.push(null); - log.type.push(null); + log.message.length = 0; + log.type.length = 0; }, assertErrorNotified: function(expected) { @@ -54,8 +54,8 @@

Prototype Unit test file

var actualType = log.type.beforeLast() || log.type.last(); this.assertEqual(expected, actualMessage, 'assertErrorNotified'); this.assertEqual('error', actualType, 'assertErrorNotified'); - log.message.push(null); - log.type.push(null); + log.message.length = 0; + log.type.length = 0; }, assertWarnNotified: function(expected) { @@ -63,21 +63,30 @@

Prototype Unit test file

var actualType = log.type.beforeLast() || log.type.last(); this.assertEqual(expected, actualMessage, 'assertWarnNotified'); this.assertEqual('warn', actualType, 'assertWarnNotified'); - log.message.push(null); - log.type.push(null); + log.message.length = 0; + log.type.length = 0; }, assertNotNotified: function() { - this.assertNull(log.message.last(), 'assertNotNotified'); - this.assertNull(log.type.last()); - log.message.push(null); - log.type.push(null); + this.assertEmpty(log.message, 'assertNotNotified'); + this.assertEmpty(log.type, 'assertNotNotified'); + log.message.length = 0; + log.type.length = 0; + }, + + assertEmpty: function(array, message) { + this.assertEqual(0, array.length, message || ''); } }); 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() { @@ -259,6 +268,15 @@

Prototype Unit test file

this.assertRespondsTo('toJSON', h) }, + testArray: function() { + var a = [0, 1, 2, 3]; + + a.reduce(function(){}); + this.assertErrorNotified('Array#reduce is no longer supported.\n' + + 'This is due to an infortunate naming collision with Mozilla\'s own implementation of Array#reduce which differs completely from Prototype\'s implementation (it\'s in fact similar to Prototype\'s Array#inject).\n' + + 'Mozilla\'s Array#reduce is already implemented in Firefox 3 (as part of JavaScript 1.8) and is about to be standardized in EcmaScript 3.1.') + }, + testClass: function() { Class.create(); this.assertInfoNotified('The class API has been fully revised and now allows for mixins and inheritance.\n' + @@ -267,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 3cbc56ae0..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([ @@ -253,6 +253,15 @@ var prototypeUpdateHelper = new UpdateHelper([ type: 'warn' }, + { + methodName: 'reduce', + namespace: Array.prototype, + message: 'Array#reduce is no longer supported.\n' + + 'This is due to an infortunate naming collision with Mozilla\'s own implementation of Array#reduce which differs completely from Prototype\'s implementation (it\'s in fact similar to Prototype\'s Array#inject).\n' + + 'Mozilla\'s Array#reduce is already implemented in Firefox 3 (as part of JavaScript 1.8) and is about to be standardized in EcmaScript 3.1.', + type: 'error' + }, + { methodName: 'unloadCache', namespace: Event, @@ -266,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' } ]); @@ -290,6 +355,7 @@ var prototypeUpdateHelper = new UpdateHelper([ } function defineSetters(obj, prop) { + storeProperties(obj); if (obj.__properties.include(prop)) return; obj.__properties.push(prop); obj.__defineGetter__(prop, function() { @@ -303,6 +369,7 @@ var prototypeUpdateHelper = new UpdateHelper([ } function checkProperties(hash) { + storeProperties(hash); var current = Object.keys(hash); if (current.length == hash.__properties.length) return; @@ -313,6 +380,12 @@ var prototypeUpdateHelper = new UpdateHelper([ }); } + function storeProperties(h) { + if (typeof h.__properties === 'undefined') + h.__properties = __properties.clone(); + return h; + } + Hash.prototype.set = Hash.prototype.set.wrap(function(proceed, property, value) { defineSetters(this, property); return proceed(property, value); @@ -335,7 +408,7 @@ var prototypeUpdateHelper = new UpdateHelper([ }); Hash.prototype.initialize = Hash.prototype.initialize.wrap(function(proceed, object) { - this.__properties = __properties.clone(); + storeProperties(this); for (var prop in object) defineSetters(this, prop); proceed(object); }); 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.js b/src/ajax.js deleted file mode 100644 index 98cf97563..000000000 --- a/src/ajax.js +++ /dev/null @@ -1,7 +0,0 @@ -//= require "ajax/ajax" -//= require "ajax/responders" -//= require "ajax/base" -//= require "ajax/request" -//= require "ajax/response" -//= require "ajax/updater" -//= require "ajax/periodical_updater" diff --git a/src/ajax/ajax.js b/src/ajax/ajax.js deleted file mode 100644 index 4895a1003..000000000 --- a/src/ajax/ajax.js +++ /dev/null @@ -1,11 +0,0 @@ -var Ajax = { - getTransport: function() { - return Try.these( - function() {return new XMLHttpRequest()}, - function() {return new ActiveXObject('Msxml2.XMLHTTP')}, - function() {return new ActiveXObject('Microsoft.XMLHTTP')} - ) || false; - }, - - activeRequestCount: 0 -}; diff --git a/src/ajax/periodical_updater.js b/src/ajax/periodical_updater.js deleted file mode 100644 index 83ea294ce..000000000 --- a/src/ajax/periodical_updater.js +++ /dev/null @@ -1,40 +0,0 @@ -Ajax.PeriodicalUpdater = Class.create(Ajax.Base, { - 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(); - }, - - start: function() { - this.options.onComplete = this.updateComplete.bind(this); - this.onTimerEvent(); - }, - - 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/ajax/request.js b/src/ajax/request.js deleted file mode 100644 index 6937b5df9..000000000 --- a/src/ajax/request.js +++ /dev/null @@ -1,172 +0,0 @@ -Ajax.Request = Class.create(Ajax.Base, { - _complete: false, - - initialize: function($super, url, options) { - $super(options); - this.transport = Ajax.getTransport(); - this.request(url); - }, - - request: function(url) { - this.url = url; - this.method = this.options.method; - var params = Object.clone(this.options.parameters); - - if (!['get', 'post'].include(this.method)) { - // simulate other verbs over post - params['_method'] = this.method; - this.method = 'post'; - } - - this.parameters = params; - - if (params = Object.toQueryString(params)) { - // when GET, append parameters to URL - if (this.method == 'get') - this.url += (this.url.include('?') ? '&' : '?') + params; - else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) - params += '&_='; - } - - try { - var response = new Ajax.Response(this); - if (this.options.onCreate) this.options.onCreate(response); - Ajax.Responders.dispatch('onCreate', this, response); - - this.transport.open(this.method.toUpperCase(), this.url, - this.options.asynchronous); - - if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1); - - this.transport.onreadystatechange = this.onStateChange.bind(this); - this.setRequestHeaders(); - - this.body = this.method == 'post' ? (this.options.postBody || params) : null; - this.transport.send(this.body); - - /* Force Firefox to handle ready state 4 for synchronous requests */ - if (!this.options.asynchronous && this.transport.overrideMimeType) - this.onStateChange(); - - } - catch (e) { - this.dispatchException(e); - } - }, - - onStateChange: function() { - var readyState = this.transport.readyState; - if (readyState > 1 && !((readyState == 4) && this._complete)) - this.respondToReadyState(this.transport.readyState); - }, - - setRequestHeaders: function() { - var headers = { - 'X-Requested-With': 'XMLHttpRequest', - 'X-Prototype-Version': Prototype.Version, - 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*' - }; - - if (this.method == 'post') { - headers['Content-type'] = this.options.contentType + - (this.options.encoding ? '; charset=' + this.options.encoding : ''); - - /* Force "Connection: close" for older Mozilla browsers to work - * around a bug where XMLHttpRequest sends an incorrect - * Content-length header. See Mozilla Bugzilla #246651. - */ - if (this.transport.overrideMimeType && - (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005) - headers['Connection'] = 'close'; - } - - // user-defined headers - if (typeof this.options.requestHeaders == 'object') { - var extras = this.options.requestHeaders; - - if (Object.isFunction(extras.push)) - for (var i = 0, length = extras.length; i < length; i += 2) - headers[extras[i]] = extras[i+1]; - else - $H(extras).each(function(pair) { headers[pair.key] = pair.value }); - } - - for (var name in headers) - this.transport.setRequestHeader(name, headers[name]); - }, - - success: function() { - var status = this.getStatus(); - return !status || (status >= 200 && status < 300); - }, - - getStatus: function() { - try { - return this.transport.status || 0; - } catch (e) { return 0 } - }, - - respondToReadyState: function(readyState) { - var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this); - - if (state == 'Complete') { - try { - this._complete = true; - (this.options['on' + response.status] - || this.options['on' + (this.success() ? 'Success' : 'Failure')] - || Prototype.emptyFunction)(response, response.headerJSON); - } catch (e) { - this.dispatchException(e); - } - - var contentType = response.getHeader('Content-type'); - if (this.options.evalJS == 'force' - || (this.options.evalJS && this.isSameOrigin() && contentType - && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i))) - this.evalResponse(); - } - - try { - (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON); - Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON); - } catch (e) { - this.dispatchException(e); - } - - if (state == 'Complete') { - // avoid memory leak in MSIE: clean up - this.transport.onreadystatechange = Prototype.emptyFunction; - } - }, - - isSameOrigin: function() { - var m = this.url.match(/^\s*https?:\/\/[^\/]*/); - return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({ - protocol: location.protocol, - domain: document.domain, - port: location.port ? ':' + location.port : '' - })); - }, - - getHeader: function(name) { - try { - return this.transport.getResponseHeader(name) || null; - } catch (e) { return null } - }, - - evalResponse: function() { - try { - return eval((this.transport.responseText || '').unfilterJSON()); - } catch (e) { - this.dispatchException(e); - } - }, - - dispatchException: function(exception) { - (this.options.onException || Prototype.emptyFunction)(this, exception); - Ajax.Responders.dispatch('onException', this, exception); - } -}); - -Ajax.Request.Events = - ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; diff --git a/src/ajax/responders.js b/src/ajax/responders.js deleted file mode 100644 index a317a9031..000000000 --- a/src/ajax/responders.js +++ /dev/null @@ -1,33 +0,0 @@ -Ajax.Responders = { - responders: [], - - _each: function(iterator) { - this.responders._each(iterator); - }, - - register: function(responder) { - if (!this.include(responder)) - this.responders.push(responder); - }, - - unregister: function(responder) { - this.responders = this.responders.without(responder); - }, - - dispatch: function(callback, request, transport, json) { - this.each(function(responder) { - if (Object.isFunction(responder[callback])) { - try { - responder[callback].apply(responder, [request, transport, json]); - } catch (e) { } - } - }); - } -}; - -Object.extend(Ajax.Responders, Enumerable); - -Ajax.Responders.register({ - onCreate: function() { Ajax.activeRequestCount++ }, - onComplete: function() { Ajax.activeRequestCount-- } -}); diff --git a/src/ajax/response.js b/src/ajax/response.js deleted file mode 100644 index c8bc2f856..000000000 --- a/src/ajax/response.js +++ /dev/null @@ -1,73 +0,0 @@ -Ajax.Response = Class.create({ - initialize: function(request){ - this.request = request; - var transport = this.transport = request.transport, - readyState = this.readyState = transport.readyState; - - if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) { - this.status = this.getStatus(); - this.statusText = this.getStatusText(); - this.responseText = String.interpret(transport.responseText); - this.headerJSON = this._getHeaderJSON(); - } - - if(readyState == 4) { - var xml = transport.responseXML; - this.responseXML = Object.isUndefined(xml) ? null : xml; - this.responseJSON = this._getResponseJSON(); - } - }, - - status: 0, - statusText: '', - - getStatus: Ajax.Request.prototype.getStatus, - - getStatusText: function() { - try { - return this.transport.statusText || ''; - } catch (e) { return '' } - }, - - getHeader: Ajax.Request.prototype.getHeader, - - getAllHeaders: function() { - try { - return this.getAllResponseHeaders(); - } catch (e) { return null } - }, - - getResponseHeader: function(name) { - return this.transport.getResponseHeader(name); - }, - - getAllResponseHeaders: function() { - return this.transport.getAllResponseHeaders(); - }, - - _getHeaderJSON: function() { - var json = this.getHeader('X-JSON'); - if (!json) return null; - json = decodeURIComponent(escape(json)); - try { - return json.evalJSON(this.request.options.sanitizeJSON || - !this.request.isSameOrigin()); - } catch (e) { - this.request.dispatchException(e); - } - }, - - _getResponseJSON: function() { - var options = this.request.options; - if (!options.evalJSON || (options.evalJSON != 'force' && - !(this.getHeader('Content-type') || '').include('application/json')) || - this.responseText.blank()) - return null; - try { - return this.responseText.evalJSON(options.sanitizeJSON || - !this.request.isSameOrigin()); - } catch (e) { - this.request.dispatchException(e); - } - } -}); diff --git a/src/ajax/updater.js b/src/ajax/updater.js deleted file mode 100644 index 262c7f6f2..000000000 --- a/src/ajax/updater.js +++ /dev/null @@ -1,35 +0,0 @@ -Ajax.Updater = Class.create(Ajax.Request, { - initialize: function($super, container, url, options) { - this.container = { - success: (container.success || container), - failure: (container.failure || (container.success ? null : container)) - }; - - options = Object.clone(options); - var onComplete = options.onComplete; - options.onComplete = (function(response, json) { - this.updateContent(response.responseText); - if (Object.isFunction(onComplete)) onComplete(response, json); - }).bind(this); - - $super(url, options); - }, - - updateContent: function(responseText) { - var receiver = this.container[this.success() ? 'success' : 'failure'], - options = this.options; - - if (!options.evalScripts) responseText = responseText.stripScripts(); - - if (receiver = $(receiver)) { - if (options.insertion) { - if (Object.isString(options.insertion)) { - var insertion = { }; insertion[options.insertion] = responseText; - receiver.insert(insertion); - } - else options.insertion(receiver, responseText); - } - else receiver.update(responseText); - } - } -}); diff --git a/src/constants.yml b/src/constants.yml index 3a05ed2f8..2532f981c 100644 --- a/src/constants.yml +++ b/src/constants.yml @@ -1 +1 @@ -PROTOTYPE_VERSION: 1.6.0.3 +PROTOTYPE_VERSION: 1.7.3 diff --git a/src/dom.js b/src/dom.js deleted file mode 100644 index c11aa68d0..000000000 --- a/src/dom.js +++ /dev/null @@ -1,6 +0,0 @@ -//= require "dom/dom" -//= require "dom/selector" -//= require "dom/form" -//= require "dom/event" - -Element.addMethods(); diff --git a/src/dom/dom.js b/src/dom/dom.js deleted file mode 100644 index 2bd17ed82..000000000 --- a/src/dom/dom.js +++ /dev/null @@ -1,1281 +0,0 @@ -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 - }); -} - -(function(global) { - var element = global.Element; - global.Element = function(tagName, attributes) { - attributes = attributes || { }; - tagName = tagName.toLowerCase(); - var cache = Element.cache; - if (Prototype.Browser.IE && 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 = { - visible: function(element) { - return $(element).style.display != 'none'; - }, - - toggle: function(element) { - element = $(element); - Element[Element.visible(element) ? 'hide' : 'show'](element); - return element; - }, - - hide: function(element) { - element = $(element); - element.style.display = 'none'; - return element; - }, - - show: function(element) { - element = $(element); - element.style.display = ''; - return element; - }, - - remove: function(element) { - element = $(element); - element.parentNode.removeChild(element); - return element; - }, - - update: function(element, content) { - element = $(element); - if (content && content.toElement) content = content.toElement(); - if (Object.isElement(content)) return element.update().insert(content); - content = Object.toHTML(content); - element.innerHTML = content.stripScripts(); - content.evalScripts.bind(content).defer(); - return element; - }, - - replace: function(element, content) { - element = $(element); - if (content && content.toElement) content = content.toElement(); - else if (!Object.isElement(content)) { - content = Object.toHTML(content); - var range = element.ownerDocument.createRange(); - range.selectNode(element); - content.evalScripts.bind(content).defer(); - content = range.createContextualFragment(content.stripScripts()); - } - element.parentNode.replaceChild(content, element); - return element; - }, - - insert: function(element, insertions) { - element = $(element); - - if (Object.isString(insertions) || Object.isNumber(insertions) || - Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML))) - insertions = {bottom:insertions}; - - var content, insert, tagName, childNodes; - - for (var position in insertions) { - content = insertions[position]; - position = position.toLowerCase(); - insert = Element._insertionTranslations[position]; - - if (content && content.toElement) content = content.toElement(); - if (Object.isElement(content)) { - insert(element, content); - continue; - } - - content = Object.toHTML(content); - - tagName = ((position == 'before' || position == 'after') - ? element.parentNode : element).tagName.toUpperCase(); - - childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); - - if (position == 'top' || position == 'after') childNodes.reverse(); - childNodes.each(insert.curry(element)); - - content.evalScripts.bind(content).defer(); - } - - return element; - }, - - wrap: function(element, wrapper, attributes) { - element = $(element); - if (Object.isElement(wrapper)) - $(wrapper).writeAttribute(attributes || { }); - else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes); - else wrapper = new Element('div', wrapper); - if (element.parentNode) - element.parentNode.replaceChild(wrapper, element); - wrapper.appendChild(element); - return wrapper; - }, - - inspect: function(element) { - element = $(element); - var result = '<' + element.tagName.toLowerCase(); - $H({'id': 'id', 'className': 'class'}).each(function(pair) { - var property = pair.first(), attribute = pair.last(); - var value = (element[property] || '').toString(); - if (value) result += ' ' + attribute + '=' + value.inspect(true); - }); - return result + '>'; - }, - - recursivelyCollect: function(element, property) { - element = $(element); - var elements = []; - while (element = element[property]) - if (element.nodeType == 1) - elements.push(Element.extend(element)); - return elements; - }, - - ancestors: function(element) { - return $(element).recursivelyCollect('parentNode'); - }, - - descendants: function(element) { - return $(element).select("*"); - }, - - firstDescendant: function(element) { - element = $(element).firstChild; - while (element && element.nodeType != 1) element = element.nextSibling; - return $(element); - }, - - immediateDescendants: function(element) { - if (!(element = $(element).firstChild)) return []; - while (element && element.nodeType != 1) element = element.nextSibling; - if (element) return [element].concat($(element).nextSiblings()); - return []; - }, - - previousSiblings: function(element) { - return $(element).recursivelyCollect('previousSibling'); - }, - - nextSiblings: function(element) { - return $(element).recursivelyCollect('nextSibling'); - }, - - siblings: function(element) { - element = $(element); - return element.previousSiblings().reverse().concat(element.nextSiblings()); - }, - - match: function(element, selector) { - if (Object.isString(selector)) - selector = new Selector(selector); - return selector.match($(element)); - }, - - up: function(element, expression, index) { - element = $(element); - if (arguments.length == 1) return $(element.parentNode); - var ancestors = element.ancestors(); - return Object.isNumber(expression) ? ancestors[expression] : - Selector.findElement(ancestors, expression, index); - }, - - down: function(element, expression, index) { - element = $(element); - if (arguments.length == 1) return element.firstDescendant(); - return Object.isNumber(expression) ? element.descendants()[expression] : - Element.select(element, expression)[index || 0]; - }, - - previous: function(element, expression, index) { - element = $(element); - if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element)); - var previousSiblings = element.previousSiblings(); - return Object.isNumber(expression) ? previousSiblings[expression] : - Selector.findElement(previousSiblings, expression, index); - }, - - next: function(element, expression, index) { - element = $(element); - if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element)); - var nextSiblings = element.nextSiblings(); - return Object.isNumber(expression) ? nextSiblings[expression] : - Selector.findElement(nextSiblings, expression, index); - }, - - select: function() { - var args = $A(arguments), element = $(args.shift()); - return Selector.findChildElements(element, args); - }, - - adjacent: function() { - var args = $A(arguments), element = $(args.shift()); - return Selector.findChildElements(element.parentNode, args).without(element); - }, - - identify: function(element) { - element = $(element); - var id = element.readAttribute('id'); - if (id) return id; - do { id = 'anonymous_element_' + Element.idCounter++ } while ($(id)); - element.writeAttribute('id', id); - return id; - }, - - readAttribute: function(element, name) { - element = $(element); - if (Prototype.Browser.IE) { - var t = Element._attributeTranslations.read; - if (t.values[name]) return t.values[name](element, name); - if (t.names[name]) name = t.names[name]; - if (name.include(':')) { - return (!element.attributes || !element.attributes[name]) ? null : - element.attributes[name].value; - } - } - - return element.getAttribute(name); - }, - - writeAttribute: function(element, name, value) { - element = $(element); - var attributes = { }, t = Element._attributeTranslations.write; - - if (typeof name == 'object') attributes = name; - else attributes[name] = Object.isUndefined(value) ? true : value; - - for (var attr in attributes) { - name = t.names[attr] || attr; - value = attributes[attr]; - if (t.values[attr]) name = t.values[attr](element, value); - if (value === false || value === null) - element.removeAttribute(name); - else if (value === true) - element.setAttribute(name, name); - else element.setAttribute(name, value); - } - return element; - }, - - getHeight: function(element) { - return $(element).getDimensions().height; - }, - - getWidth: function(element) { - return $(element).getDimensions().width; - }, - - classNames: function(element) { - return new Element.ClassNames(element); - }, - - hasClassName: function(element, className) { - if (!(element = $(element))) return; - var elementClassName = element.className; - return (elementClassName.length > 0 && (elementClassName == className || - new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName))); - }, - - addClassName: function(element, className) { - if (!(element = $(element))) return; - if (!element.hasClassName(className)) - element.className += (element.className ? ' ' : '') + className; - return element; - }, - - removeClassName: function(element, className) { - if (!(element = $(element))) return; - element.className = element.className.replace( - new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip(); - return element; - }, - - toggleClassName: function(element, className) { - if (!(element = $(element))) return; - return element[element.hasClassName(className) ? - 'removeClassName' : 'addClassName'](className); - }, - - // removes whitespace-only text node children - cleanWhitespace: function(element) { - element = $(element); - var node = element.firstChild; - while (node) { - var nextNode = node.nextSibling; - if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) - element.removeChild(node); - node = nextNode; - } - return element; - }, - - empty: function(element) { - return $(element).innerHTML.blank(); - }, - - descendantOf: function(element, ancestor) { - element = $(element), ancestor = $(ancestor); - - if (element.compareDocumentPosition) - return (element.compareDocumentPosition(ancestor) & 8) === 8; - - if (ancestor.contains) - return ancestor.contains(element) && ancestor !== element; - - while (element = element.parentNode) - if (element == ancestor) return true; - - return false; - }, - - scrollTo: function(element) { - element = $(element); - var pos = element.cumulativeOffset(); - window.scrollTo(pos[0], pos[1]); - return element; - }, - - getStyle: function(element, style) { - element = $(element); - style = style == 'float' ? 'cssFloat' : style.camelize(); - var value = element.style[style]; - if (!value || value == 'auto') { - 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; - }, - - getOpacity: function(element) { - return $(element).getStyle('opacity'); - }, - - setStyle: function(element, styles) { - element = $(element); - var elementStyle = element.style, match; - if (Object.isString(styles)) { - element.style.cssText += ';' + styles; - return styles.include('opacity') ? - element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element; - } - for (var property in styles) - if (property == 'opacity') element.setOpacity(styles[property]); - else - elementStyle[(property == 'float' || property == 'cssFloat') ? - (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') : - property] = styles[property]; - - return element; - }, - - setOpacity: function(element, value) { - element = $(element); - element.style.opacity = (value == 1 || value === '') ? '' : - (value < 0.00001) ? 0 : value; - return element; - }, - - getDimensions: function(element) { - element = $(element); - var display = element.getStyle('display'); - if (display != 'none' && display != null) // Safari bug - return {width: element.offsetWidth, height: element.offsetHeight}; - - // All *Width and *Height properties give 0 on elements with display none, - // so enable the element temporarily - var els = element.style; - var originalVisibility = els.visibility; - var originalPosition = els.position; - var originalDisplay = els.display; - els.visibility = 'hidden'; - if (originalPosition != 'fixed') // Switching fixed to absolute causes issues in Safari - els.position = 'absolute'; - els.display = 'block'; - var originalWidth = element.clientWidth; - var originalHeight = element.clientHeight; - els.display = originalDisplay; - els.position = originalPosition; - els.visibility = originalVisibility; - return {width: originalWidth, height: originalHeight}; - }, - - makePositioned: function(element) { - element = $(element); - var pos = Element.getStyle(element, 'position'); - if (pos == 'static' || !pos) { - element._madePositioned = true; - element.style.position = 'relative'; - // Opera returns the offset relative to the positioning context, when an - // element is position relative but top and left have not been defined - if (Prototype.Browser.Opera) { - element.style.top = 0; - element.style.left = 0; - } - } - return element; - }, - - undoPositioned: function(element) { - element = $(element); - if (element._madePositioned) { - element._madePositioned = undefined; - element.style.position = - element.style.top = - element.style.left = - element.style.bottom = - element.style.right = ''; - } - return element; - }, - - makeClipping: function(element) { - element = $(element); - if (element._overflow) return element; - element._overflow = Element.getStyle(element, 'overflow') || 'auto'; - if (element._overflow !== 'hidden') - element.style.overflow = 'hidden'; - return element; - }, - - undoClipping: function(element) { - element = $(element); - if (!element._overflow) return element; - element.style.overflow = element._overflow == 'auto' ? '' : element._overflow; - element._overflow = null; - return element; - }, - - cumulativeOffset: function(element) { - var valueT = 0, valueL = 0; - do { - valueT += element.offsetTop || 0; - valueL += element.offsetLeft || 0; - element = element.offsetParent; - } while (element); - return Element._returnOffset(valueL, valueT); - }, - - positionedOffset: function(element) { - var valueT = 0, valueL = 0; - do { - valueT += element.offsetTop || 0; - valueL += element.offsetLeft || 0; - element = element.offsetParent; - if (element) { - if (element.tagName.toUpperCase() == 'BODY') break; - var p = Element.getStyle(element, 'position'); - if (p !== 'static') break; - } - } while (element); - return Element._returnOffset(valueL, valueT); - }, - - absolutize: function(element) { - element = $(element); - if (element.getStyle('position') == 'absolute') return element; - // Position.prepare(); // To be done manually by Scripty when it needs it. - - var offsets = element.positionedOffset(); - var top = offsets[1]; - var left = offsets[0]; - var width = element.clientWidth; - var height = element.clientHeight; - - element._originalLeft = left - parseFloat(element.style.left || 0); - element._originalTop = top - parseFloat(element.style.top || 0); - element._originalWidth = element.style.width; - element._originalHeight = element.style.height; - - element.style.position = 'absolute'; - element.style.top = top + 'px'; - element.style.left = left + 'px'; - element.style.width = width + 'px'; - element.style.height = height + 'px'; - return element; - }, - - relativize: function(element) { - element = $(element); - if (element.getStyle('position') == 'relative') return element; - // Position.prepare(); // To be done manually by Scripty when it needs it. - - element.style.position = 'relative'; - var top = parseFloat(element.style.top || 0) - (element._originalTop || 0); - var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); - - element.style.top = top + 'px'; - element.style.left = left + 'px'; - element.style.height = element._originalHeight; - element.style.width = element._originalWidth; - return element; - }, - - cumulativeScrollOffset: function(element) { - var valueT = 0, valueL = 0; - do { - valueT += element.scrollTop || 0; - valueL += element.scrollLeft || 0; - element = element.parentNode; - } while (element); - return Element._returnOffset(valueL, valueT); - }, - - getOffsetParent: function(element) { - if (element.offsetParent) return $(element.offsetParent); - if (element == document.body) return $(element); - - while ((element = element.parentNode) && element != document.body) - if (Element.getStyle(element, 'position') != 'static') - return $(element); - - return $(document.body); - }, - - viewportOffset: function(forElement) { - var valueT = 0, valueL = 0; - - var element = forElement; - do { - valueT += element.offsetTop || 0; - valueL += element.offsetLeft || 0; - - // Safari fix - if (element.offsetParent == document.body && - Element.getStyle(element, 'position') == 'absolute') break; - - } while (element = element.offsetParent); - - element = forElement; - do { - if (!Prototype.Browser.Opera || (element.tagName && (element.tagName.toUpperCase() == 'BODY'))) { - valueT -= element.scrollTop || 0; - valueL -= element.scrollLeft || 0; - } - } while (element = element.parentNode); - - return Element._returnOffset(valueL, valueT); - }, - - clonePosition: function(element, source) { - var options = Object.extend({ - setLeft: true, - setTop: true, - setWidth: true, - setHeight: true, - offsetTop: 0, - offsetLeft: 0 - }, arguments[2] || { }); - - // find page position of source - source = $(source); - var p = source.viewportOffset(); - - // find coordinate system to use - element = $(element); - var delta = [0, 0]; - var parent = null; - // delta [0,0] will do fine with position: fixed elements, - // position:absolute needs offsetParent deltas - if (Element.getStyle(element, 'position') == 'absolute') { - parent = element.getOffsetParent(); - delta = parent.viewportOffset(); - } - - // correct by body offsets (fixes Safari) - if (parent == document.body) { - delta[0] -= document.body.offsetLeft; - delta[1] -= document.body.offsetTop; - } - - // set position - if (options.setLeft) element.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px'; - if (options.setTop) element.style.top = (p[1] - delta[1] + options.offsetTop) + 'px'; - if (options.setWidth) element.style.width = source.offsetWidth + 'px'; - if (options.setHeight) element.style.height = source.offsetHeight + 'px'; - return element; - } -}; - -Object.extend(Element.Methods, { - getElementsBySelector: Element.Methods.select, - childElements: Element.Methods.immediateDescendants -}); - -Element._attributeTranslations = { - write: { - names: { - className: 'class', - htmlFor: 'for' - }, - values: { } - } -}; - -if (Prototype.Browser.Opera) { - Element.Methods.getStyle = Element.Methods.getStyle.wrap( - function(proceed, element, style) { - switch (style) { - case 'left': case 'top': case 'right': case 'bottom': - if (proceed(element, 'position') === 'static') return null; - case 'height': case 'width': - // returns '0px' for hidden elements; we want it to return null - if (!Element.visible(element)) return null; - - // returns the border-box dimensions rather than the content-box - // dimensions, so we subtract padding and borders from the value - var dim = parseInt(proceed(element, style), 10); - - if (dim !== element['offset' + style.capitalize()]) - return dim + 'px'; - - var properties; - if (style === 'height') { - properties = ['border-top-width', 'padding-top', - 'padding-bottom', 'border-bottom-width']; - } - else { - properties = ['border-left-width', 'padding-left', - 'padding-right', 'border-right-width']; - } - return properties.inject(dim, function(memo, property) { - var val = proceed(element, property); - return val === null ? memo : memo - parseInt(val, 10); - }) + 'px'; - default: return proceed(element, style); - } - } - ); - - Element.Methods.readAttribute = Element.Methods.readAttribute.wrap( - function(proceed, element, attribute) { - if (attribute === 'title') return element.title; - return proceed(element, attribute); - } - ); -} - -else if (Prototype.Browser.IE) { - // IE doesn't report offsets correctly for static elements, so we change them - // to "relative" to get the values, then change them back. - Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap( - function(proceed, element) { - element = $(element); - // IE throws an error if element is not in document - try { element.offsetParent } - catch(e) { return $(document.body) } - var position = element.getStyle('position'); - if (position !== 'static') return proceed(element); - element.setStyle({ position: 'relative' }); - var value = proceed(element); - element.setStyle({ position: position }); - return value; - } - ); - - $w('positionedOffset viewportOffset').each(function(method) { - Element.Methods[method] = Element.Methods[method].wrap( - function(proceed, element) { - element = $(element); - try { element.offsetParent } - catch(e) { return Element._returnOffset(0,0) } - var position = element.getStyle('position'); - if (position !== 'static') return proceed(element); - // Trigger hasLayout on the offset parent so that IE6 reports - // accurate offsetTop and offsetLeft values for position: fixed. - var offsetParent = element.getOffsetParent(); - if (offsetParent && offsetParent.getStyle('position') === 'fixed') - offsetParent.setStyle({ zoom: 1 }); - element.setStyle({ position: 'relative' }); - var value = proceed(element); - element.setStyle({ position: position }); - return value; - } - ); - }); - - Element.Methods.cumulativeOffset = Element.Methods.cumulativeOffset.wrap( - function(proceed, element) { - try { element.offsetParent } - catch(e) { return Element._returnOffset(0,0) } - return proceed(element); - } - ); - - Element.Methods.getStyle = function(element, style) { - element = $(element); - style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize(); - var value = element.style[style]; - if (!value && element.currentStyle) value = element.currentStyle[style]; - - if (style == 'opacity') { - if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/)) - if (value[1]) return parseFloat(value[1]) / 100; - return 1.0; - } - - if (value == 'auto') { - if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none')) - return element['offset' + style.capitalize()] + 'px'; - return null; - } - return value; - }; - - Element.Methods.setOpacity = function(element, value) { - function stripAlpha(filter){ - return filter.replace(/alpha\([^\)]*\)/gi,''); - } - element = $(element); - var currentStyle = element.currentStyle; - if ((currentStyle && !currentStyle.hasLayout) || - (!currentStyle && element.style.zoom == 'normal')) - element.style.zoom = 1; - - var filter = element.getStyle('filter'), style = element.style; - if (value == 1 || value === '') { - (filter = stripAlpha(filter)) ? - style.filter = filter : style.removeAttribute('filter'); - return element; - } else if (value < 0.00001) value = 0; - style.filter = stripAlpha(filter) + - 'alpha(opacity=' + (value * 100) + ')'; - return element; - }; - - Element._attributeTranslations = { - read: { - names: { - 'class': 'className', - 'for': 'htmlFor' - }, - values: { - _getAttr: function(element, attribute) { - return element.getAttribute(attribute, 2); - }, - _getAttrNode: function(element, attribute) { - var node = element.getAttributeNode(attribute); - return node ? node.value : ""; - }, - _getEv: function(element, attribute) { - attribute = element.getAttribute(attribute); - - // TODO: Need something less ugly here. - if (!attribute) return null; - attribute = attribute.toString(); - attribute = attribute.split('{')[1]; - attribute = attribute.split('}')[0]; - return attribute.strip(); - }, - _flag: function(element, attribute) { - return $(element).hasAttribute(attribute) ? attribute : null; - }, - style: function(element) { - return element.style.cssText.toLowerCase(); - }, - title: function(element) { - return element.title; - } - } - } - }; - - Element._attributeTranslations.write = { - names: Object.extend({ - cellpadding: 'cellPadding', - cellspacing: 'cellSpacing' - }, Element._attributeTranslations.read.names), - values: { - checked: function(element, value) { - element.checked = !!value; - }, - - style: function(element, value) { - element.style.cssText = value ? value : ''; - } - } - }; - - Element._attributeTranslations.has = {}; - - $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' + - 'encType maxLength readOnly longDesc frameBorder').each(function(attr) { - Element._attributeTranslations.write.names[attr.toLowerCase()] = attr; - Element._attributeTranslations.has[attr.toLowerCase()] = attr; - }); - - (function(v) { - Object.extend(v, { - href: v._getAttr, - src: v._getAttr, - type: v._getAttr, - action: v._getAttrNode, - disabled: v._flag, - checked: v._flag, - readonly: v._flag, - multiple: v._flag, - onload: v._getEv, - onunload: v._getEv, - onclick: v._getEv, - ondblclick: v._getEv, - onmousedown: v._getEv, - onmouseup: v._getEv, - onmouseover: v._getEv, - onmousemove: v._getEv, - onmouseout: v._getEv, - onfocus: v._getEv, - onblur: v._getEv, - onkeypress: v._getEv, - onkeydown: v._getEv, - onkeyup: v._getEv, - onsubmit: v._getEv, - onreset: v._getEv, - onselect: v._getEv, - onchange: v._getEv - }); - })(Element._attributeTranslations.read.values); -} - -else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) { - Element.Methods.setOpacity = function(element, value) { - element = $(element); - element.style.opacity = (value == 1) ? 0.999999 : - (value === '') ? '' : (value < 0.00001) ? 0 : value; - return element; - }; -} - -else if (Prototype.Browser.WebKit) { - Element.Methods.setOpacity = function(element, value) { - element = $(element); - element.style.opacity = (value == 1 || value === '') ? '' : - (value < 0.00001) ? 0 : value; - - if (value == 1) - if(element.tagName.toUpperCase() == 'IMG' && element.width) { - element.width++; element.width--; - } else try { - var n = document.createTextNode(' '); - element.appendChild(n); - element.removeChild(n); - } catch (e) { } - - return element; - }; - - // Safari returns margins on body which is incorrect if the child is absolutely - // positioned. For performance reasons, redefine Element#cumulativeOffset for - // KHTML/WebKit only. - Element.Methods.cumulativeOffset = function(element) { - var valueT = 0, valueL = 0; - do { - valueT += element.offsetTop || 0; - valueL += element.offsetLeft || 0; - if (element.offsetParent == document.body) - if (Element.getStyle(element, 'position') == 'absolute') break; - - element = element.offsetParent; - } while (element); - - return Element._returnOffset(valueL, valueT); - }; -} - -if (Prototype.Browser.IE || Prototype.Browser.Opera) { - // IE and Opera are missing .innerHTML support for TABLE-related and SELECT elements - Element.Methods.update = function(element, content) { - element = $(element); - - 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 (tagName in Element._insertionTranslations.tags) { - $A(element.childNodes).each(function(node) { element.removeChild(node) }); - Element._getContentFromAnonymousElement(tagName, content.stripScripts()) - .each(function(node) { element.appendChild(node) }); - } - else element.innerHTML = content.stripScripts(); - - content.evalScripts.bind(content).defer(); - return element; - }; -} - -if ('outerHTML' in document.createElement('div')) { - Element.Methods.replace = function(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 (Element._insertionTranslations.tags[tagName]) { - var nextSibling = element.next(); - var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); - parent.removeChild(element); - if (nextSibling) - fragments.each(function(node) { parent.insertBefore(node, nextSibling) }); - else - fragments.each(function(node) { parent.appendChild(node) }); - } - else element.outerHTML = content.stripScripts(); - - content.evalScripts.bind(content).defer(); - return element; - }; -} - -Element._returnOffset = function(l, t) { - var result = [l, t]; - result.left = l; - result.top = t; - return result; -}; - -Element._getContentFromAnonymousElement = function(tagName, html) { - var div = new Element('div'), t = Element._insertionTranslations.tags[tagName]; - if (t) { - div.innerHTML = t[0] + html + t[1]; - t[2].times(function() { div = div.firstChild }); - } else div.innerHTML = html; - return $A(div.childNodes); -}; - -Element._insertionTranslations = { - before: function(element, node) { - element.parentNode.insertBefore(node, element); - }, - top: function(element, node) { - element.insertBefore(node, element.firstChild); - }, - bottom: function(element, node) { - element.appendChild(node); - }, - after: function(element, node) { - element.parentNode.insertBefore(node, element.nextSibling); - }, - tags: { - TABLE: ['', '
', 1], - TBODY: ['', '
', 2], - TR: ['', '
', 3], - TD: ['
', '
', 4], - SELECT: ['', 1] - } -}; - -(function() { - Object.extend(this.tags, { - THEAD: this.tags.TBODY, - TFOOT: this.tags.TBODY, - TH: this.tags.TD - }); -}).call(Element._insertionTranslations); - -Element.Methods.Simulated = { - hasAttribute: function(element, attribute) { - attribute = Element._attributeTranslations.has[attribute] || attribute; - var node = $(element).getAttributeNode(attribute); - return !!(node && node.specified); - } -}; - -Element.Methods.ByTag = { }; - -Object.extend(Element, Element.Methods); - -(function(div) { - - if (!Prototype.BrowserFeatures.ElementExtensions && div['__proto__']) { - window.HTMLElement = { }; - window.HTMLElement.prototype = div['__proto__']; - Prototype.BrowserFeatures.ElementExtensions = true; - } - - div = null; - -})(document.createElement('div')) - -Element.extend = (function() { - if (Prototype.BrowserFeatures.SpecificElementExtensions) - return Prototype.K; - - var Methods = { }, ByTag = Element.Methods.ByTag; - - var extend = Object.extend(function(element) { - if (!element || element._extendedByPrototype || - element.nodeType != 1 || element == window) return element; - - var methods = Object.clone(Methods), - tagName = element.tagName.toUpperCase(), property, value; - - // extend methods for specific tags - if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]); - - for (property in methods) { - value = methods[property]; - if (Object.isFunction(value) && !(property in element)) - element[property] = value.methodize(); - } - - element._extendedByPrototype = Prototype.emptyFunction; - return element; - - }, { - refresh: function() { - // extend methods for all tags (Safari doesn't need this) - if (!Prototype.BrowserFeatures.ElementExtensions) { - Object.extend(Methods, Element.Methods); - Object.extend(Methods, Element.Methods.Simulated); - } - } - }); - - extend.refresh(); - return extend; -})(); - -Element.hasAttribute = function(element, attribute) { - if (element.hasAttribute) return element.hasAttribute(attribute); - return Element.Methods.Simulated.hasAttribute(element, attribute); -}; - -Element.addMethods = function(methods) { - var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag; - - if (!methods) { - Object.extend(Form, Form.Methods); - Object.extend(Form.Element, Form.Element.Methods); - Object.extend(Element.Methods.ByTag, { - "FORM": Object.clone(Form.Methods), - "INPUT": Object.clone(Form.Element.Methods), - "SELECT": Object.clone(Form.Element.Methods), - "TEXTAREA": Object.clone(Form.Element.Methods) - }); - } - - if (arguments.length == 2) { - var tagName = methods; - methods = arguments[1]; - } - - if (!tagName) Object.extend(Element.Methods, methods || { }); - else { - if (Object.isArray(tagName)) tagName.each(extend); - else extend(tagName); - } - - function extend(tagName) { - tagName = tagName.toUpperCase(); - if (!Element.Methods.ByTag[tagName]) - Element.Methods.ByTag[tagName] = { }; - Object.extend(Element.Methods.ByTag[tagName], methods); - } - - function copy(methods, destination, onlyIfAbsent) { - onlyIfAbsent = onlyIfAbsent || false; - for (var property in methods) { - var value = methods[property]; - if (!Object.isFunction(value)) continue; - if (!onlyIfAbsent || !(property in destination)) - destination[property] = value.methodize(); - } - } - - function findDOMClass(tagName) { - var klass; - var trans = { - "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph", - "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList", - "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading", - "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote", - "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION": - "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD": - "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR": - "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET": - "FrameSet", "IFRAME": "IFrame" - }; - if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element'; - if (window[klass]) return window[klass]; - klass = 'HTML' + tagName + 'Element'; - if (window[klass]) return window[klass]; - klass = 'HTML' + tagName.capitalize() + 'Element'; - if (window[klass]) return window[klass]; - - var element = document.createElement(tagName); - var proto = element['__proto__'] || element.constructor.prototype; - element = null; - return proto; - } - - var elementPrototype = window.HTMLElement ? HTMLElement.prototype : - Element.prototype; - - if (F.ElementExtensions) { - copy(Element.Methods, elementPrototype); - copy(Element.Methods.Simulated, elementPrototype, true); - } - - if (F.SpecificElementExtensions) { - for (var tag in Element.Methods.ByTag) { - var klass = findDOMClass(tag); - if (Object.isUndefined(klass)) continue; - copy(T[tag], klass.prototype); - } - } - - Object.extend(Element, Element.Methods); - delete Element.ByTag; - - if (Element.extend.refresh) Element.extend.refresh(); - Element.cache = { }; -}; - -document.viewport = { - getDimensions: function() { - return { width: this.getWidth(), height: this.getHeight() }; - }, - - getScrollOffsets: function() { - return Element._returnOffset( - window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft, - window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop); - } -}; - -(function(viewport) { - var B = Prototype.Browser, doc = document, element, property = {}; - - function getRootElement() { - // Older versions of Safari. - if (B.WebKit && !doc.evaluate) - return document; - - // Older versions of Opera. - if (B.Opera && window.parseFloat(window.opera.version()) < 9.5) - return document.body; - - return document.documentElement; - } - - function define(D) { - if (!element) element = getRootElement(); - - property[D] = 'client' + D; - - viewport['get' + D] = function() { return element[property[D]] }; - return viewport['get' + D](); - } - - viewport.getWidth = define.curry('Width'); - viewport.getHeight = define.curry('Height'); -})(document.viewport); - - -Element.Storage = { - UID: 1 -}; - -Element.addMethods({ - getStorage: function(element) { - if (!(element = $(element))) return; - - var uid; - if (element === window) { - uid = 0; - } else { - if (Object.isUndefined(element._prototypeUID)) - element._prototypeUID = [Element.Storage.UID++]; - uid = element._prototypeUID[0]; - } - - if (!Element.Storage[uid]) - Element.Storage[uid] = $H(); - - return Element.Storage[uid]; - }, - - store: function(element, key, value) { - if (!(element = $(element))) return; - - if (arguments.length === 2) { - // Assume we've been passed an object full of key/value pairs. - element.getStorage().update(key); - } else { - element.getStorage().set(key, value); - } - - return element; - }, - - retrieve: function(element, key, defaultValue) { - if (!(element = $(element))) return; - var hash = Element.getStorage(element), value = hash.get(key); - - if (Object.isUndefined(value)) { - hash.set(key, defaultValue); - value = defaultValue; - } - - return value; - } -}); diff --git a/src/dom/event.js b/src/dom/event.js deleted file mode 100644 index d1922f632..000000000 --- a/src/dom/event.js +++ /dev/null @@ -1,412 +0,0 @@ -(function() { - var Event = { - KEY_BACKSPACE: 8, - KEY_TAB: 9, - KEY_RETURN: 13, - KEY_ESC: 27, - KEY_LEFT: 37, - KEY_UP: 38, - KEY_RIGHT: 39, - KEY_DOWN: 40, - KEY_DELETE: 46, - KEY_HOME: 36, - KEY_END: 35, - KEY_PAGEUP: 33, - KEY_PAGEDOWN: 34, - KEY_INSERT: 45, - - cache: {} - }; - - var _isButton; - if (Prototype.Browser.IE) { - // IE doesn't map left/right/middle the same way. - var buttonMap = { 0: 1, 1: 4, 2: 2 }; - _isButton = function(event, code) { - return event.button === buttonMap[code]; - }; - } else if (Prototype.Browser.WebKit) { - // In Safari we have to account for when the user holds down - // the "meta" key. - _isButton = function(event, code) { - switch (code) { - case 0: return event.which == 1 && !event.metaKey; - case 1: return event.which == 1 && event.metaKey; - default: return false; - } - }; - } else { - _isButton = function(event, code) { - return event.which ? (event.which === code + 1) : (event.button === code); - }; - } - - function isLeftClick(event) { return _isButton(event, 0) } - function isMiddleClick(event) { return _isButton(event, 1) } - function isRightClick(event) { return _isButton(event, 2) } - - function element(event) { - event = Event.extend(event); - - var node = event.target, type = event.type, - currentTarget = event.currentTarget; - - if (currentTarget && currentTarget.tagName) { - // Firefox screws up the "click" event when moving between radio buttons - // via arrow keys. It also screws up the "load" and "error" events on images, - // reporting the document as the target instead of the original image. - if (type === 'load' || type === 'error' || - (type === 'click' && currentTarget.tagName.toLowerCase() === 'input' - && currentTarget.type === 'radio')) - node = currentTarget; - } - - // Fix a Safari bug where a text node gets passed as the target of an - // anchor click rather than the anchor itself. - if (node.nodeType == Node.TEXT_NODE) - node = node.parentNode; - - return Element.extend(node); - } - - function findElement(event, expression) { - var element = Event.element(event); - if (!expression) return element; - var elements = [element].concat(element.ancestors()); - return Selector.findElement(elements, expression, 0); - } - - function pointer(event) { - var docElement = document.documentElement, - body = document.body || { scrollLeft: 0, scrollTop: 0 }; - return { - x: event.pageX || (event.clientX + - (docElement.scrollLeft || body.scrollLeft) - - (docElement.clientLeft || 0)), - y: event.pageY || (event.clientY + - (docElement.scrollTop || body.scrollTop) - - (docElement.clientTop || 0)) - }; - } - - function pointerX(event) { return Event.pointer(event).x } - function pointerY(event) { return Event.pointer(event).y } - - function stop(event) { - Event.extend(event); - event.preventDefault(); - event.stopPropagation(); - - // Set a "stopped" property so that a custom event can be inspected - // after the fact to determine whether or not it was stopped. - event.stopped = true; - } - - Event.Methods = { - isLeftClick: isLeftClick, - isMiddleClick: isMiddleClick, - isRightClick: isRightClick, - - element: element, - findElement: findElement, - - pointer: pointer, - pointerX: pointerX, - pointerY: pointerY, - - stop: stop - }; - - - // Compile the list of methods that get extended onto Events. - var methods = Object.keys(Event.Methods).inject({ }, function(m, name) { - m[name] = Event.Methods[name].methodize(); - return m; - }); - - if (Prototype.Browser.IE) { - function _relatedTarget(event) { - var element; - switch (event.type) { - case 'mouseover': element = event.fromElement; break; - case 'mouseout': element = event.toElement; break; - default: return null; - } - return Element.extend(element); - } - - Object.extend(methods, { - stopPropagation: function() { this.cancelBubble = true }, - preventDefault: function() { this.returnValue = false }, - inspect: function() { return '[object Event]' } - }); - - // IE's method for extending events. - Event.extend = function(event) { - if (!event) return false; - if (event._extendedByPrototype) return event; - - event._extendedByPrototype = Prototype.emptyFunction; - var pointer = Event.pointer(event); - Object.extend(event, { - target: event.srcElement, - relatedTarget: _relatedTarget(event), - pageX: pointer.x, - pageY: pointer.y - }); - return Object.extend(event, methods); - }; - } else { - Event.prototype = window.Event.prototype || document.createEvent('HTMLEvents').__proto__; - Object.extend(Event.prototype, methods); - Event.extend = Prototype.K; - } - - function _createResponder(element, eventName, handler) { - // We don't set a default on the call to Element#retrieve so that we can - // handle the element's "virgin" state. - var registry = Element.retrieve(element, 'prototype_event_registry'); - - if (Object.isUndefined(registry)) { - // First time we've handled this element. Put it into the cache. - CACHE.push(element); - registry = Element.retrieve(element, 'prototype_event_registry', $H()); - } - - var respondersForEvent = registry.get(eventName); - if (Object.isUndefined()) { - respondersForEvent = []; - registry.set(eventName, respondersForEvent); - } - - // Work around the issue that permits a handler to be attached more than - // once to the same element & event type. - if (respondersForEvent.pluck('handler').include(handler)) return false; - - var responder; - if (eventName.include(":")) { - // Custom event. - responder = function(event) { - // If it's not a custom event, ignore it. - if (Object.isUndefined(event.eventName)) - return false; - - // If it's a custom event, but not the _correct_ custom event, ignore it. - if (event.eventName !== eventName) - return false; - - Event.extend(event); - handler.call(element, event); - }; - } else { - // Ordinary event. - responder = function(event) { - Event.extend(event); - handler.call(element, event); - }; - } - - responder.handler = handler; - respondersForEvent.push(responder); - return responder; - } - - function _destroyCache() { - for (var i = 0, length = CACHE.length; i < length; i++) - Event.stopObserving(CACHE[i]); - } - - var CACHE = []; - - // Internet Explorer needs to remove event handlers on page unload - // in order to avoid memory leaks. - if (Prototype.Browser.IE) - window.attachEvent('onunload', _destroyCache); - - // Safari needs a dummy event handler on page unload so that it won't - // use its bfcache. Safari <= 3.1 has an issue with restoring the "document" - // object when page is returned to via the back button using its bfcache. - if (Prototype.Browser.WebKit) - window.addEventListener('unload', Prototype.emptyFunction, false); - - - function observe(element, eventName, handler) { - element = $(element); - - var responder = _createResponder(element, eventName, handler); - - if (!responder) return element; - - if (eventName.include(':')) { - // Custom event. - if (element.addEventListener) - element.addEventListener("dataavailable", responder, false); - else { - // We observe two IE-proprietarty events: one for custom events that - // bubble and one for custom events that do not bubble. - element.attachEvent("ondataavailable", responder); - element.attachEvent("onfilterchange", responder); - } - } else { - // Ordinary event. - if (element.addEventListener) - element.addEventListener(eventName, responder, false); - else - element.attachEvent("on" + eventName, responder); - } - - return element; - } - - function stopObserving(element, eventName, handler) { - element = $(element); - - var registry = Element.retrieve(element, 'prototype_event_registry'); - - if (Object.isUndefined(registry)) return element; - - if (eventName && !handler) { - // If an event name is passed without a handler, we stop observing all - // handlers of that type. - var responders = registry.get(eventName); - - if (Object.isUndefined(responders)) return element; - - responders.each( function(r) { - Element.stopObserving(element, eventName, r.handler); - }); - return element; - } else if (!eventName) { - // If both the event name and the handler are omitted, we stop observing - // _all_ handlers on the element. - registry.each( function(pair) { - var eventName = pair.key, responders = pair.value; - - responders.each( function(r) { - Element.stopObserving(element, eventName, r.handler); - }); - }); - return element; - } - - var responders = registry.get(eventName); - var responder = responders.find( function(r) { return r.handler === handler; }); - if (!responder) return element; - - if (eventName.include(':')) { - // Custom event. - if (element.removeEventListener) - element.removeEventListener("dataavailable", responder, false); - else { - element.detachEvent("ondataavailable", responder); - element.detachEvent("onfilterchange", responder); - } - } else { - // Ordinary event. - if (element.removeEventListener) - element.removeEventListener(eventName, responder, false); - else - element.detachEvent('on' + eventName, responder); - } - - registry.set(eventName, responders.without(responder)); - - return element; - } - - function fire(element, eventName, memo, bubble) { - element = $(element); - - if (Object.isUndefined(bubble)) - bubble = true; - - if (element == document && document.createEvent && !element.dispatchEvent) - element = document.documentElement; - - var event; - if (document.createEvent) { - event = document.createEvent('HTMLEvents'); - event.initEvent('dataavailable', true, true); - } else { - event = document.createEventObject(); - event.eventType = bubble ? 'ondataavailable' : 'onfilterchange'; - } - - event.eventName = eventName; - event.memo = memo || { }; - - if (document.createEvent) - element.dispatchEvent(event); - else - element.fireEvent(event.eventType, event); - - return Event.extend(event); - } - - - Object.extend(Event, Event.Methods); - - Object.extend(Event, { - fire: fire, - observe: observe, - stopObserving: stopObserving - }); - - Element.addMethods({ - fire: fire, - observe: observe, - stopObserving: stopObserving - }); - - Object.extend(document, { - fire: fire.methodize(), - observe: observe.methodize(), - stopObserving: stopObserving.methodize(), - loaded: false - }); - - // Export to the global scope. - if (window.Event) Object.extend(window.Event, Event); - else window.Event = Event; -})(); - -(function() { - /* Support for the DOMContentLoaded event is based on work by Dan Webb, - Matthias Miller, Dean Edwards, John Resig, and Diego Perini. */ - - var timer; - - function fireContentLoadedEvent() { - if (document.loaded) return; - if (timer) window.clearTimeout(timer); - document.loaded = true; - document.fire('dom:loaded'); - } - - function checkReadyState() { - if (document.readyState === 'complete') { - document.stopObserving('readystatechange', checkReadyState); - fireContentLoadedEvent(); - } - } - - function pollDoScroll() { - try { document.documentElement.doScroll('left'); } - catch(e) { - timer = pollDoScroll.defer(); - return; - } - fireContentLoadedEvent(); - } - - if (document.addEventListener) { - document.addEventListener('DOMContentLoaded', fireContentLoadedEvent, false); - } else { - document.observe('readystatechange', checkReadyState); - if (window === top) - timer = pollDoScroll.defer(); - } - - // Worst-case fallback - Event.observe(window, 'load', fireContentLoadedEvent); -})(); diff --git a/src/dom/form.js b/src/dom/form.js deleted file mode 100644 index 68c440061..000000000 --- a/src/dom/form.js +++ /dev/null @@ -1,339 +0,0 @@ -var Form = { - reset: function(form) { - $(form).reset(); - return form; - }, - - serializeElements: function(elements, options) { - if (typeof options != 'object') options = { hash: !!options }; - else if (Object.isUndefined(options.hash)) options.hash = true; - var key, value, submitted = false, submit = options.submit; - - var data = elements.inject({ }, function(result, element) { - if (!element.disabled && element.name) { - key = element.name; value = $(element).getValue(); - if (value != null && element.type != 'file' && (element.type != 'submit' || (!submitted && - submit !== false && (!submit || key == submit) && (submitted = true)))) { - if (key in result) { - // a key is already present; construct an array of values - if (!Object.isArray(result[key])) result[key] = [result[key]]; - result[key].push(value); - } - else result[key] = value; - } - } - return result; - }); - - return options.hash ? data : Object.toQueryString(data); - } -}; - -Form.Methods = { - serialize: function(form, options) { - return Form.serializeElements(Form.getElements(form), options); - }, - - getElements: function(form) { - return $A($(form).getElementsByTagName('*')).inject([], - function(elements, child) { - if (Form.Element.Serializers[child.tagName.toLowerCase()]) - elements.push(Element.extend(child)); - return elements; - } - ); - }, - - getInputs: function(form, typeName, name) { - form = $(form); - var inputs = form.getElementsByTagName('input'); - - if (!typeName && !name) return $A(inputs).map(Element.extend); - - for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) { - var input = inputs[i]; - if ((typeName && input.type != typeName) || (name && input.name != name)) - continue; - matchingInputs.push(Element.extend(input)); - } - - return matchingInputs; - }, - - disable: function(form) { - form = $(form); - Form.getElements(form).invoke('disable'); - return form; - }, - - enable: function(form) { - form = $(form); - Form.getElements(form).invoke('enable'); - return form; - }, - - findFirstElement: function(form) { - var elements = $(form).getElements().findAll(function(element) { - return 'hidden' != element.type && !element.disabled; - }); - var firstByIndex = elements.findAll(function(element) { - return element.hasAttribute('tabIndex') && element.tabIndex >= 0; - }).sortBy(function(element) { return element.tabIndex }).first(); - - return firstByIndex ? firstByIndex : elements.find(function(element) { - return ['input', 'select', 'textarea'].include(element.tagName.toLowerCase()); - }); - }, - - focusFirstElement: function(form) { - form = $(form); - form.findFirstElement().activate(); - return form; - }, - - request: function(form, options) { - form = $(form), options = Object.clone(options || { }); - - var params = options.parameters, action = form.readAttribute('action') || ''; - if (action.blank()) action = window.location.href; - options.parameters = form.serialize(true); - - if (params) { - if (Object.isString(params)) params = params.toQueryParams(); - Object.extend(options.parameters, params); - } - - if (form.hasAttribute('method') && !options.method) - options.method = form.method; - - return new Ajax.Request(action, options); - } -}; - -/*--------------------------------------------------------------------------*/ - -Form.Element = { - focus: function(element) { - $(element).focus(); - return element; - }, - - select: function(element) { - $(element).select(); - return element; - } -}; - -Form.Element.Methods = { - serialize: function(element) { - element = $(element); - if (!element.disabled && element.name) { - var value = element.getValue(); - if (value != undefined) { - var pair = { }; - pair[element.name] = value; - return Object.toQueryString(pair); - } - } - return ''; - }, - - getValue: function(element) { - element = $(element); - var method = element.tagName.toLowerCase(); - return Form.Element.Serializers[method](element); - }, - - setValue: function(element, value) { - element = $(element); - var method = element.tagName.toLowerCase(); - Form.Element.Serializers[method](element, value); - return element; - }, - - clear: function(element) { - $(element).value = ''; - return element; - }, - - present: function(element) { - return $(element).value != ''; - }, - - activate: function(element) { - element = $(element); - try { - element.focus(); - if (element.select && (element.tagName.toLowerCase() != 'input' || - !['button', 'reset', 'submit'].include(element.type))) - element.select(); - } catch (e) { } - return element; - }, - - disable: function(element) { - element = $(element); - element.disabled = true; - return element; - }, - - enable: function(element) { - element = $(element); - element.disabled = false; - return element; - } -}; - -/*--------------------------------------------------------------------------*/ - -var Field = Form.Element; -var $F = Form.Element.Methods.getValue; - -/*--------------------------------------------------------------------------*/ - -Form.Element.Serializers = { - input: function(element, value) { - switch (element.type.toLowerCase()) { - case 'checkbox': - case 'radio': - return Form.Element.Serializers.inputSelector(element, value); - default: - return Form.Element.Serializers.textarea(element, value); - } - }, - - inputSelector: function(element, value) { - if (Object.isUndefined(value)) return element.checked ? element.value : null; - else element.checked = !!value; - }, - - textarea: function(element, value) { - if (Object.isUndefined(value)) return element.value; - else element.value = value; - }, - - select: function(element, value) { - if (Object.isUndefined(value)) - return this[element.type == 'select-one' ? - 'selectOne' : 'selectMany'](element); - else { - 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); - } - } - }, - - selectOne: function(element) { - var index = element.selectedIndex; - return index >= 0 ? this.optionValue(element.options[index]) : null; - }, - - selectMany: function(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(this.optionValue(opt)); - } - return values; - }, - - optionValue: function(opt) { - // extend element because hasAttribute may not be native - return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text; - } -}; - -/*--------------------------------------------------------------------------*/ - -Abstract.TimedObserver = Class.create(PeriodicalExecuter, { - 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; - } - } -}); - -Form.Element.Observer = Class.create(Abstract.TimedObserver, { - getValue: function() { - return Form.Element.getValue(this.element); - } -}); - -Form.Observer = Class.create(Abstract.TimedObserver, { - getValue: function() { - return Form.serialize(this.element); - } -}); - -/*--------------------------------------------------------------------------*/ - -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; - } - } - } -}); - -Form.Element.EventObserver = Class.create(Abstract.EventObserver, { - getValue: function() { - return Form.Element.getValue(this.element); - } -}); - -Form.EventObserver = Class.create(Abstract.EventObserver, { - getValue: function() { - return Form.serialize(this.element); - } -}); diff --git a/src/lang.js b/src/lang.js deleted file mode 100644 index e21a1806e..000000000 --- a/src/lang.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * == lang == - * Language extensions. -**/ - -var Abstract = { }; - -/** section: lang - * Try -**/ - -/** - * 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. - **/ -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; - } -}; - -//= require "lang/class" -//= require "lang/object" -//= require "lang/function" -//= require "lang/date" -//= 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" diff --git a/src/lang/array.js b/src/lang/array.js deleted file mode 100644 index 5144aa112..000000000 --- a/src/lang/array.js +++ /dev/null @@ -1,306 +0,0 @@ -/** section: lang, alias of: Array.from - * $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. - **/ -function $A(iterable) { - if (!iterable) return []; - if (iterable.toArray) return iterable.toArray(); - var length = iterable.length || 0, results = new Array(length); - while (length--) results[length] = iterable[length]; - return results; -} - -if (Prototype.Browser.WebKit) { - $A = function(iterable) { - if (!iterable) return []; - // In Safari, only use the `toArray` method if it's not a NodeList. - // A NodeList is a function, has an function `item` property, and a numeric - // `length` property. Adapted from Google Doctype. - if (!(typeof iterable === 'function' && typeof iterable.length === - 'number' && typeof iterable.item === 'function') && iterable.toArray) - return iterable.toArray(); - var length = iterable.length || 0, results = new Array(length); - while (length--) results[length] = iterable[length]; - return results; - }; -} - -/** section: lang - * $w(string) -> Array - * - string (String): A string with zero or more spaces. - * - * Splits a string into an array, treating all whitespace as delimiters. - * - * Equivalent to Ruby's `%w{foo bar}` or Perl's `qw(foo bar)`. -**/ -function $w(string) { - if (!Object.isString(string)) return []; - string = string.strip(); - return string ? string.split(/\s+/) : []; -} - -Array.from = $A; - -/** section: lang - * Array -**/ -(function() { - var arrayProto = Array.prototype, - 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; - - /** - * Array#clear() -> Array - * Empties an array. - **/ - function clear() { - this.length = 0; - return this; - } - - /** - * Array#first() -> ? - * Returns the array's first item. - **/ - function first() { - return this[0]; - } - - /** - * Array#last() -> ? - * Returns the array's last item. - **/ - function last() { - return this[this.length - 1]; - } - - /** - * Array#compact() -> Array - * Trims the array of `null`, `undefined`, or other "falsy" values. - **/ - function compact() { - return this.select(function(value) { - return value != null; - }); - } - - /** - * Array#flatten() -> Array - * Returns a “flat” (one-dimensional) version of the array. - * - * Nested arrays are recursively injected “inline”. This can prove very - * useful when handling the results of a recursive collection algorithm, - * for instance. - **/ - function flatten() { - return this.inject([], function(array, value) { - if (Object.isArray(value)) - return array.concat(value.flatten()); - array.push(value); - return array; - }); - } - - /** - * Array#without(value...) -> Array - * - value (?): A value to exclude. - * - * Produces a new version of the array that does not contain any of the - * specified values. - **/ - function without() { - var values = slice.call(arguments, 0); - return this.select(function(value) { - return !values.include(value); - }); - } - - /** - * Array#reverse([inline = false]) -> Array - * - inline (Boolean): Whether to modify the array in place. If `false`, - * clones the original array first. - * - * Returns the reversed version of the array. - **/ - function reverse(inline) { - return (inline !== false ? this : this.toArray())._reverse(); - } - - /** - * Array#reduce() -> Array - * Reduces arrays: one-element arrays are turned into their unique item, - * while multiple-element arrays are returned untouched. - **/ - function reduce() { - return this.length > 1 ? this : this[0]; - } - - /** - * Array#uniq([sorted = false]) -> Array - * - sorted (Boolean): Whether the array has already been sorted. If `true`, - * 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. - **/ - function uniq(sorted) { - return this.inject([], function(array, value, index) { - if (0 == index || (sorted ? array.last() != value : !array.include(value))) - array.push(value); - return array; - }); - } - - /** - * Array#intersect(array) -> Array - * - array (Array): A collection of values. - * - * Returns an array containing every item that is shared between the two - * given arrays. - **/ - function intersect(array) { - return this.uniq().findAll(function(item) { - return array.detect(function(value) { return item === value }); - }); - } - - /** alias of: Array#toArray - * Array#clone() -> Array - * - * Returns a duplicate of the array, leaving the original array intact. - **/ - function clone() { - return slice.call(this, 0); - } - - /** related to: Enumerable#size - * Array#size() -> Number - * Returns the size of the array. - * - * This is just a local optimization of the mixed-in [[Enumerable#size]] - * which avoids array cloning and uses the array’s native length property. - **/ - function size() { - return this.length; - } - - /** related to: Object.inspect - * Array#inspect() -> String - * - * Returns the debug-oriented string representation of an array. - **/ - function inspect() { - return '[' + this.map(Object.inspect).join(', ') + ']'; - } - - /** related to: Object.toJSON - * Array#toJSON() -> String - * - * Returns a JSON string representation of the array. - **/ - 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 position of the first occurrence of `item` within the array — or - * `-1` if `item` doesn’t exist in the array. - **/ - 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; - } - - /** 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; - } - - /** - * Array#concat(args...) -> Array - * - * TODO: Array#concat - **/ - 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]); - } else { - array.push(item); - } - } - return array; - } - - Object.extend(arrayProto, Enumerable); - - if (!arrayProto._reverse) - arrayProto._reverse = arrayProto.reverse; - - Object.extend(arrayProto, { - _each: _each, - clear: clear, - first: first, - last: last, - compact: compact, - flatten: flatten, - without: without, - reverse: reverse, - reduce: reduce, - uniq: uniq, - intersect: intersect, - clone: clone, - toArray: clone, - size: size, - inspect: inspect, - toJSON: toJSON - }); - - // 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/lang/class.js deleted file mode 100644 index 0e3bd4a10..000000000 --- a/src/lang/class.js +++ /dev/null @@ -1,111 +0,0 @@ -/* Based on Alex Arnell's inheritance implementation. */ - -/** section: lang - * Class -**/ -var Class = (function() { - /** - * Class.create([superclass][, methods...]) -> Class - * - superclass (Class): The optional superclass to inherit methods from. - * - methods (Object): An object whose properties will be "mixed-in" to the - * new class. Any number of mixins can be added; later mixins take - * precedence. - * - * Creates a class. - * - * Class.create returns a function that, when called, will fire its own - * `initialize` method. - * - * `Class.create` accepts two kinds of arguments. If the first argument is - * a `Class`, it's treated 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 as instance methods of the new - * class. Later arguments take precedence over earlier arguments. - * - * If a subclass overrides an instance method declared in a superclass, the - * subclass's method can still access the original method. To do so, declare - * the subclass's method as normal, but insert `$super` as the first - * argument. This makes `$super` available as a method for use within the - * function. - * - * To extend a class after it has been defined, use [[Class#addMethods]]. - **/ - function create() { - var parent = null, properties = $A(arguments); - if (Object.isFunction(properties[0])) - parent = properties.shift(); - - function klass() { - this.initialize.apply(this, arguments); - } - - Object.extend(klass, Class.Methods); - klass.superclass = parent; - klass.subclasses = []; - - if (parent) { - var subclass = function() {}; - subclass.prototype = parent.prototype; - klass.prototype = new subclass; - parent.subclasses.push(klass); - } - - for (var i = 0; i < properties.length; i++) - klass.addMethods(properties[i]); - - if (!klass.prototype.initialize) - klass.prototype.initialize = Prototype.emptyFunction; - - klass.prototype.constructor = klass; - return klass; - } - - /** - * Class#addMethods(methods) -> Class - * - methods (Object): The methods to add to the class. - * - * 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 - * to that class, or overwrite existing methods, after the class has been - * defined. - * - * New methods propagate down the inheritance chain. If the class has - * subclasses, those subclasses will receive the new methods — even in the - * context of `$super` calls. The new methods also propagate to instances of - * the class and of all its subclasses, even those that have already been - * instantiated. - **/ - function addMethods(source) { - var ancestor = this.superclass && this.superclass.prototype; - var properties = Object.keys(source); - - if (!Object.keys({ toString: true }).length) - properties.push("toString", "valueOf"); - - 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") { - 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); - } - this.prototype[property] = value; - } - - return this; - } - - return { - create: create, - Methods: { - addMethods: addMethods - } - }; -})(); \ No newline at end of file diff --git a/src/lang/date.js b/src/lang/date.js deleted file mode 100644 index f0c67d187..000000000 --- a/src/lang/date.js +++ /dev/null @@ -1,18 +0,0 @@ -/** section: lang - * Date -**/ - -/** - * Date#toJSON -> String - * - * TODO: Date#toJSON -**/ -Date.prototype.toJSON = function() { - return '"' + this.getUTCFullYear() + '-' + - (this.getUTCMonth() + 1).toPaddedString(2) + '-' + - this.getUTCDate().toPaddedString(2) + 'T' + - this.getUTCHours().toPaddedString(2) + ':' + - this.getUTCMinutes().toPaddedString(2) + ':' + - this.getUTCSeconds().toPaddedString(2) + 'Z"'; -}; - diff --git a/src/lang/enumerable.js b/src/lang/enumerable.js deleted file mode 100644 index 3cb908c1d..000000000 --- a/src/lang/enumerable.js +++ /dev/null @@ -1,384 +0,0 @@ -/** section: lang - * mixin Enumerable -**/ - -var $break = { }; - -var Enumerable = (function() { - /** - * Enumerable#each(iterator[, context]) -> Enumerable - * - iterator (Function): A `Function` that expects an item in the - * collection as the first argument and a numerical index as the second. - * - context (Object): The scope in which to call `iterator`. Affects what - * the keyword `this` means inside `iterator`. - * - * Calls `iterator` for each item in the collection. - **/ - function each(iterator, context) { - var index = 0; - try { - this._each(function(value) { - iterator.call(context, value, index++); - }); - } catch (e) { - if (e != $break) throw e; - } - return this; - } - - /** - * Enumerable#eachSlice(number[, iterator = Prototype.K[, context]]) -> Enumerable - * - * TODO: Enumerable#eachSlice - * http://prototypejs.org/api/enumerable#method-eachslice - **/ - function eachSlice(number, iterator, context) { - var index = -number, slices = [], array = this.toArray(); - if (number < 1) return array; - while ((index += number) < array.length) - slices.push(array.slice(index, index+number)); - return slices.collect(iterator, context); - } - - /** - * Enumerable#all([iterator = Prototype.K[, context]]) -> Boolean - * - * Determines whether all the elements are boolean-equivalent to `true`, - * either directly or through computation by the provided iterator. - **/ - function all(iterator, context) { - iterator = iterator || Prototype.K; - var result = true; - this.each(function(value, index) { - result = result && !!iterator.call(context, value, index); - if (!result) throw $break; - }); - return result; - } - - /** - * Enumerable#any([iterator = Prototype.K[, context]]) -> Boolean - * - * Determines whether at least one element is boolean-equivalent to `true`, - * either directly or through computation by the provided iterator. - **/ - function any(iterator, context) { - iterator = iterator || Prototype.K; - var result = false; - this.each(function(value, index) { - if (result = !!iterator.call(context, value, index)) - throw $break; - }); - return result; - } - - /** alias of: Enumerable#map - * Enumerable#collect([iterator = Prototype.K[, context]]) -> Array - * - * Returns the results of applying the iterator to each element. - * Aliased as [[Enumerable#map]]. - **/ - function collect(iterator, context) { - iterator = iterator || Prototype.K; - var results = []; - this.each(function(value, index) { - results.push(iterator.call(context, value, index)); - }); - return results; - } - - /** alias of: Enumerable#find - * Enumerable#detect(iterator[, context]) -> firstElement | undefined - * - * Finds the first element for which the iterator returns a “truthy” value. - * Aliased by the [[Enumerable#find]] method. - **/ - function detect(iterator, context) { - var result; - this.each(function(value, index) { - if (iterator.call(context, value, index)) { - result = value; - throw $break; - } - }); - return result; - } - - /** alias of: select - * Enumerable#findAll(iterator[, context]) -> Array - * - * Returns all the elements for which the iterator returned “truthy” value. - * Aliased as [[Enumerable#select]]. - **/ - function findAll(iterator, context) { - var results = []; - this.each(function(value, index) { - if (iterator.call(context, value, index)) - results.push(value); - }); - return results; - } - - /** - * Enumerable#grep(regex[, iterator = Prototype.K[, context]]) -> Array - * - * Returns all the elements that match the filter. If an iterator is provided, - * it is used to produce the returned value for each selected element. - **/ - function grep(filter, iterator, context) { - iterator = iterator || Prototype.K; - var results = []; - - if (Object.isString(filter)) - filter = new RegExp(RegExp.escape(filter)); - - this.each(function(value, index) { - if (filter.match(value)) - results.push(iterator.call(context, value, index)); - }); - return results; - } - - /** alias of: Enumerable#member - * Enumerable#include(object) -> Boolean - * - * Determines whether a given object is in the Enumerable or not, - * based on the `==` comparison operator. Aliased as [[Enumerable#member]]. - **/ - function include(object) { - if (Object.isFunction(this.indexOf)) - if (this.indexOf(object) != -1) return true; - - var found = false; - this.each(function(value) { - if (value == object) { - found = true; - throw $break; - } - }); - return found; - } - - /** - * Enumerable#inGroupsOf(size[, filler = null]) -> [group...] - * - * Groups items in fixed-size chunks, using a specific value to fill up - * the last chunk if necessary. - **/ - function inGroupsOf(number, fillWith) { - fillWith = Object.isUndefined(fillWith) ? null : fillWith; - return this.eachSlice(number, function(slice) { - while(slice.length < number) slice.push(fillWith); - return slice; - }); - } - - /** - * Enumerable#inject(accumulator, iterator[, context]) -> accumulatedValue - * - * Incrementally builds a result value based on the successive results - * of the iterator. - * This can be used for array construction, numerical sums/averages, etc. - **/ - function inject(memo, iterator, context) { - this.each(function(value, index) { - memo = iterator.call(context, memo, value, index); - }); - return memo; - } - - /** - * Enumerable#invoke(methodName[, arg...]) -> Array - * - * Invokes the same method, with the same arguments, for all items in a collection. - * Returns the results of the method calls. - **/ - function invoke(method) { - var args = $A(arguments).slice(1); - return this.map(function(value) { - return value[method].apply(value, args); - }); - } - - /** - * Enumerable#max([iterator = Prototype.K[, context]]) -> maxValue - * - * Returns the maximum element (or element-based computation), or undefined if - * the enumeration is empty. - * Elements are either compared directly, or by first applying the iterator - * and comparing returned values. - **/ - function max(iterator, context) { - iterator = iterator || Prototype.K; - var result; - this.each(function(value, index) { - value = iterator.call(context, value, index); - if (result == null || value >= result) - result = value; - }); - return result; - } - - /** - * Enumerable#min([iterator = Prototype.K[, context]]) -> minValue - * - * Returns the minimum element (or element-based computation), or undefined if - * the enumeration is empty. - * Elements are either compared directly, or by first applying the iterator - * and comparing returned values. - **/ - function min(iterator, context) { - iterator = iterator || Prototype.K; - var result; - this.each(function(value, index) { - value = iterator.call(context, value, index); - if (result == null || value < result) - result = value; - }); - return result; - } - - /** - * Enumerable#partition([iterator = Prototype.K[, context]]) -> [TrueArray, FalseArray] - * - * Partitions the elements in two groups: those regarded as true, and those - * considered false. - * By default, regular JavaScript boolean equivalence is used, but an iterator - * can be provided, that computes a boolean representation of the elements. - **/ - function partition(iterator, context) { - iterator = iterator || Prototype.K; - var trues = [], falses = []; - this.each(function(value, index) { - (iterator.call(context, value, index) ? - trues : falses).push(value); - }); - return [trues, falses]; - } - - /** - * Enumerable#pluck(propertyName) -> Array - * - * Optimization for a common use-case of collect: fetching the same property - * for all the elements. Returns the property values. - **/ - function pluck(property) { - var results = []; - this.each(function(value) { - results.push(value[property]); - }); - return results; - } - - /** - * Enumerable#reject(iterator[, context]) -> Array - * - * Returns all the elements for which the iterator returned a “falsy” value. - **/ - function reject(iterator, context) { - var results = []; - this.each(function(value, index) { - if (!iterator.call(context, value, index)) - results.push(value); - }); - return results; - } - - /** - * Enumerable#sortBy(iterator[, context]) -> Array - * - * Provides a custom-sorted view of the elements based on the criteria computed, - * for each element, by the iterator. - **/ - function sortBy(iterator, context) { - return this.map(function(value, index) { - return { - value: value, - criteria: iterator.call(context, value, index) - }; - }).sort(function(left, right) { - var a = left.criteria, b = right.criteria; - return a < b ? -1 : a > b ? 1 : 0; - }).pluck('value'); - } - - /** alias of: Enumerable#entries - * Enumerable#toArray() -> Array - * - * Returns an Array representation of the enumeration. - * Aliased as [[Enumerable#entries]]. - **/ - function toArray() { - return this.map(); - } - - /** - * Enumerable#zip(sequence...[, iterator = Prototype.K]) -> Array - * - * Zips together (think of the zip on a pair of trousers) 2+ sequences, - * providing an array of tuples. - * Each tuple contains one value per original sequence. - * Tuples can be converted to something else by applying the optional iterator on them. - **/ - function zip() { - var iterator = Prototype.K, args = $A(arguments); - if (Object.isFunction(args.last())) - iterator = args.pop(); - - var collections = [this].concat(args).map($A); - return this.map(function(value, index) { - return iterator(collections.pluck(index)); - }); - } - - /** - * Enumerable#size() -> Number - * - * Returns the size of the enumeration. - **/ - function size() { - return this.toArray().length; - } - - /** related to: Object.inspect - * Enumerable#inspect() -> String - * - * TODO: Enumerable#inspect - **/ - function inspect() { - return '#'; - } - - return { - each: each, - eachSlice: eachSlice, - all: all, - every: all, - any: any, - some: any, - collect: collect, - map: collect, - detect: detect, - findAll: findAll, - select: findAll, - filter: findAll, - grep: grep, - include: include, - member: include, - inGroupsOf: inGroupsOf, - inject: inject, - invoke: invoke, - max: max, - min: min, - partition: partition, - pluck: pluck, - reject: reject, - sortBy: sortBy, - toArray: toArray, - entries: toArray, - zip: zip, - size: size, - inspect: inspect, - find: detect - }; -})(); diff --git a/src/lang/function.js b/src/lang/function.js deleted file mode 100644 index 435c02a6a..000000000 --- a/src/lang/function.js +++ /dev/null @@ -1,165 +0,0 @@ -/** section: lang - * Function -**/ -Object.extend(Function.prototype, (function() { - var slice = Array.prototype.slice; - - function update(array, args) { - var arrayLength = array.length, length = args.length; - while (length--) array[arrayLength + length] = args[length]; - return array; - } - - function merge(array, args) { - array = slice.call(array, 0); - return update(array, args); - } - - /** - * Function#argumentNames() -> Array - * - * Reads the argument names as stated in the function definition and returns - * the values as an array of strings (or an empty array if the function is - * defined without parameters). - **/ - function argumentNames() { - var names = this.toString().match(/^[\s\(]*function[^(]*\(([^)]*)\)/)[1] - .replace(/\/\/.*?[\r\n]|\/\*(?:.|[\r\n])*?\*\//g, '') - .replace(/\s+/g, '').split(','); - return names.length == 1 && !names[0] ? [] : names; - } - - /** - * Function#bind(object[, args...]) -> Function - * - object (Object): The object to bind to. - * - * Wraps the function in another, locking its execution scope to an object - * specified by `object`. - **/ - function bind(context) { - if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this; - var __method = this, args = slice.call(arguments, 1); - return function() { - var a = merge(args, arguments); - return __method.apply(context, a); - } - } - - /** related to: Function#bind - * Function#bindAsEventListener(object[, args...]) -> Function - * - object (Object): The object to bind to. - * - * An event-specific variant of [[Function#bind]] which ensures the function - * will recieve the current event object as the first argument when - * executing. - **/ - function bindAsEventListener(context) { - var __method = this, args = slice.call(arguments, 1); - return function(event) { - var a = update([event || window.event], args); - return __method.apply(context, a); - } - } - - /** - * Function#curry(args...) -> Function - * Partially applies the function, returning a function with one or more - * arguments already “filled in.” - * - * Function#curry works just like [[Function#bind]] without the initial - * scope argument. Use the latter if you need to partially apply a function - * _and_ modify its execution scope at the same time. - **/ - function curry() { - if (!arguments.length) return this; - var __method = this, args = slice.call(arguments, 0); - return function() { - var a = merge(args, arguments); - return __method.apply(this, a); - } - } - - /** - * Function#delay(seconds[, args...]) -> Number - * - seconds (Number): How long to wait before calling the function. - * - * Schedules the function to run after the specified amount of time, passing - * any arguments given. - * - * Behaves much like `window.setTimeout`. Returns an integer ID that can be - * used to clear the timeout with `window.clearTimeout` before it runs. - * - * To schedule a function to run as soon as the interpreter is idle, use - * [[Function#defer]]. - **/ - function delay(timeout) { - var __method = this, args = slice.call(arguments, 1); - timeout = timeout * 1000 - return window.setTimeout(function() { - return __method.apply(__method, args); - }, timeout); - } - - /** - * Function#defer(args...) -> Number - * Schedules the function to run as soon as the interpreter is idle. - * - * A “deferred” function will not run immediately; rather, it will run as soon - * as the interpreter’s call stack is empty. - * - * Behaves much like `window.setTimeout` with a delay set to `0`. Returns an - * ID that can be used to clear the timeout with `window.clearTimeout` before - * it runs. - **/ - function defer() { - var args = update([0.01], arguments); - return this.delay.apply(this, args); - } - - /** - * Function#wrap(wrapperFunction) -> Function - * - wrapperFunction (Function): The function to act as a wrapper. - * - * Returns a function “wrapped” around the original function. - * - * `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. - **/ - function wrap(wrapper) { - var __method = this; - return function() { - var a = update([__method.bind(this)], arguments); - return wrapper.apply(this, a); - } - } - - /** - * Function#methodize() -> Function - * Wraps the function inside another function that, at call time, pushes - * `this` to the original function as the first argument. - * - * Used to define both a generic method and an instance method. - **/ - function methodize() { - if (this._methodized) return this._methodized; - var __method = this; - return this._methodized = function() { - var a = update([this], arguments); - return __method.apply(null, a); - }; - } - - return { - argumentNames: argumentNames, - bind: bind, - bindAsEventListener: bindAsEventListener, - curry: curry, - delay: delay, - defer: defer, - wrap: wrap, - methodize: methodize - } -})()); - diff --git a/src/lang/hash.js b/src/lang/hash.js deleted file mode 100644 index 0a275b0fa..000000000 --- a/src/lang/hash.js +++ /dev/null @@ -1,204 +0,0 @@ -/** section: lang - * $H([object]) -> 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). - **/ -function $H(object) { - return new Hash(object); -}; - -/** section: lang - * class Hash -**/ -var Hash = Class.create(Enumerable, (function() { - /** - * new Hash([object]) - * - * TODO: new Hash - **/ - function initialize(object) { - this._object = Object.isHash(object) ? object.toObject() : Object.clone(object); - } - - function _each(iterator) { - for (var key in this._object) { - var value = this._object[key], pair = [key, value]; - pair.key = key; - pair.value = value; - iterator(pair); - } - } - - /** - * Hash#set(key, value) -> value - * - * Sets the hash’s `key` property to value and returns value. - **/ - function set(key, value) { - return this._object[key] = value; - } - - /** - * Hash#get(key) -> value - * - * Returns the value of the hash’s `key` property. - **/ - function get(key) { - // simulating poorly supported hasOwnProperty - if (this._object[key] !== Object.prototype[key]) - return this._object[key]; - } - - /** - * Hash#unset(key) -> value - * - * Deletes the hash’s `key` property and returns its value. - **/ - function unset(key) { - var value = this._object[key]; - delete this._object[key]; - return value; - } - - /** - * Hash#toObject() -> Object - * - * Returns a cloned, vanilla object. - **/ - function toObject() { - return Object.clone(this._object); - } - - /** - * Hash#keys() -> [String...] - * - * Provides an Array of keys (that is, property names) for the hash. - **/ - function keys() { - return this.pluck('key'); - } - - /** - * Hash#values() -> Array - * - * Collect the values of a hash and returns them in an array. - **/ - function values() { - return this.pluck('value'); - } - - /** - * Hash#index(value) -> String - * - * TODO: Hash#index - **/ - function index(value) { - var match = this.detect(function(pair) { - return pair.value === value; - }); - return match && match.key; - } - - /** - * Hash#merge(object) -> Hash - * - * Merges `object` to hash and returns the result of that merge. - * Prior to v1.6.0: This was destructive (object's values were added to hash). - * Since v1.6.0: This is no longer destructive (hash is cloned before the operation). - **/ - function merge(object) { - return this.clone().update(object); - } - - /** - * Hash#update(object) -> Hash - * - * Updates hash with the key/value pairs of `object`. - * The original hash will be modified. - **/ - function update(object) { - return new Hash(object).inject(this, function(result, pair) { - result.set(pair.key, pair.value); - return result; - }); - } - - /** - * Hash#toQueryPair(key, value) -> String - * - * TODO: Hash#toQueryPair - **/ - function toQueryPair(key, value) { - if (Object.isUndefined(value)) return key; - return key + '=' + encodeURIComponent(String.interpret(value)); - } - - /** related to: String#toQueryParams - * Hash#toQueryString() -> String - * - * Turns a hash into its URL-encoded query string representation. - **/ - 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))); - } else results.push(toQueryPair(key, values)); - return results; - }).join('&'); - } - - /** related to: Object.inspect - * Hash#inspect() -> String - * - * Returns the debug-oriented string representation of the hash. - **/ - function inspect() { - return '#'; - } - - /** related to: Object.toJSON - * Hash#toJSON() -> String - * - * Returns a JSON string. - **/ - function toJSON() { - return Object.toJSON(this.toObject()); - } - - /** - * Hash#clone() -> newHash - * - * Returns a clone of hash. - **/ - function clone() { - return new Hash(this); - } - - return { - initialize: initialize, - _each: _each, - set: set, - get: get, - unset: unset, - toObject: toObject, - toTemplateReplacements: toObject, - keys: keys, - values: values, - index: index, - merge: merge, - update: update, - toQueryString: toQueryString, - inspect: inspect, - toJSON: toJSON, - clone: clone - }; -})()); - -Hash.from = $H; diff --git a/src/lang/number.js b/src/lang/number.js deleted file mode 100644 index 6d3c78047..000000000 --- a/src/lang/number.js +++ /dev/null @@ -1,104 +0,0 @@ -/** section: lang - * Number -**/ -Object.extend(Number.prototype, (function() { - /** - * Number#toColorPart() -> String - * - * Produces a 2-digit hexadecimal representation of the number - * (which is therefore assumed to be in the [0..255] range). - * Useful for composing CSS color strings. - **/ - function toColorPart() { - return this.toPaddedString(2, 16); - } - - /** - * Number#succ() -> Number - * - * Returns the successor of the current Number, as defined by current + 1. - * Used to make numbers compatible with ObjectRange. - **/ - function succ() { - return this + 1; - } - - /** - * Number#times(iterator) -> Number - * - * Encapsulates a regular [0..n[ loop, Ruby-style. - **/ - function times(iterator, context) { - $R(0, this, true).each(iterator, context); - return this; - } - - /** - * Number#toPaddedString(length[, radix]) -> String - * - * Converts the number into a string padded with 0s so that the string’s length - * is at least equal to `length`. - * Takes an optional `radix` argument which specifies the base to use for conversion. - **/ - function toPaddedString(length, radix) { - var string = this.toString(radix || 10); - return '0'.times(length - string.length) + string; - } - - /** related to: Object.toJSON - * Number#toJSON() -> String - * - * Returns a JSON string. - **/ - function toJSON() { - return isFinite(this) ? this.toString() : 'null'; - } - - /** - * Number#abs() -> Number - * - * Returns the absolute value of the number. - **/ - function abs() { - return Math.abs(this); - } - - /** - * Number#round() -> Number - * - * Rounds the number to the nearest integer. - **/ - function round() { - return Math.round(this); - } - - /** - * Number#ceil() -> Number - * - * Returns the smallest integer greater than or equal to the number. - **/ - function ceil() { - return Math.ceil(this); - } - - /** - * Number#floor() -> Number - * - * Returns the largest integer less than or equal to the number. - **/ - function floor() { - return Math.floor(this); - } - - return { - toColorPart: toColorPart, - succ: succ, - times: times, - toPaddedString: toPaddedString, - toJSON: toJSON, - abs: abs, - round: round, - ceil: ceil, - floor: floor - }; -})()); diff --git a/src/lang/object.js b/src/lang/object.js deleted file mode 100644 index 7ff1c8db2..000000000 --- a/src/lang/object.js +++ /dev/null @@ -1,266 +0,0 @@ -/** section: lang - * Object -**/ -(function() { - - function getClass(object) { - return Object.prototype.toString.call(object) - .match(/^\[object\s(.*)\]$/)[1]; - } - - /** - * 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. Returns - * the destination object. - **/ - function extend(destination, source) { - for (var property in source) - destination[property] = source[property]; - return destination; - } - - /** - * Object.inspect(object) -> 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 checked 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 — among them `String`, `Array`, `Enumerable` and `Hash`. - * These attempt to provide useful string representations (from a - * developer’s standpoint) for their respective types. - **/ - 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`. - **/ - function toJSON(object) { - var type = typeof object; - switch (type) { - case 'undefined': - case 'function': - case 'unknown': return; - case 'boolean': return object.toString(); - } - - if (object === null) return 'null'; - if (object.toJSON) return object.toJSON(); - if (isElement(object)) return; - - var results = []; - for (var property in object) { - var value = toJSON(object[property]); - if (!isUndefined(value)) - results.push(property.toJSON() + ': ' + value); - } - - return '{' + results.join(', ') + '}'; - } - - /** - * 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. - **/ - 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]]. - **/ - function toHTML(object) { - return object && object.toHTML ? object.toHTML() : String.interpret(object); - } - - /** - * Object.keys(object) -> Array - * - object (Object): The object to pull keys from. - * - * Returns an array of the object's property names. - * - * 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. Sort the resulting array if you wish to - * normalize the order of the object keys. - **/ - function keys(object) { - var results = []; - for (var property in object) - results.push(property); - return results; - } - - /** - * Object.values(object) -> Array - * - object (Object): The object to pull values from. - * - * Returns an array of the object's 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. - **/ - 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. - * - * Duplicates the passed object. - * - * Copies all 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. - **/ - 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. - **/ - 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. - **/ - function isArray(object) { - return getClass(object) === "Array"; - } - - - /** - * Object.isHash(object) -> Boolean - * - object (Object): The object to test. - * - * Returns `true` if `object` is an instance of the [[Hash]] class; `false` - * otherwise. - **/ - 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. - **/ - function isFunction(object) { - return typeof object === "function"; - } - - /** - * Object.isString(object) -> Boolean - * - object (Object): The object to test. - * - * Returns `true` if `object` is of type `string`; `false` otherwise. - **/ - function isString(object) { - return getClass(object) === "String"; - } - - /** - * Object.isNumber(object) -> Boolean - * - object (Object): The object to test. - * - * Returns `true` if `object` is of type `number`; `false` otherwise. - **/ - function isNumber(object) { - return getClass(object) === "Number"; - } - - /** - * Object.isUndefined(object) -> Boolean - * - object (Object): The object to test. - * - * Returns `true` if `object` is of type `string`; `false` otherwise. - **/ - function isUndefined(object) { - return typeof object === "undefined"; - } - - extend(Object, { - extend: extend, - inspect: inspect, - toJSON: toJSON, - toQueryString: toQueryString, - toHTML: toHTML, - keys: keys, - values: values, - clone: clone, - isElement: isElement, - isArray: isArray, - isHash: isHash, - isFunction: isFunction, - isString: isString, - isNumber: isNumber, - isUndefined: isUndefined - }); -})(); diff --git a/src/lang/periodical_executer.js b/src/lang/periodical_executer.js deleted file mode 100644 index cb753bb89..000000000 --- a/src/lang/periodical_executer.js +++ /dev/null @@ -1,61 +0,0 @@ -/** section: lang - * class PeriodicalExecuter -**/ -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. - * - * Creates an object that oversees the calling of a particular function via - * `window.setInterval`. - * - * The only notable advantage provided by `PeriodicalExecuter` is that it - * shields you against multiple parallel executions of the `callback` - * function, should it take longer than the given interval to execute (it - * maintains an internal “running” flag, which is shielded against - * exceptions in the callback function). - * - * 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. - **/ - initialize: function(callback, frequency) { - this.callback = callback; - this.frequency = frequency; - this.currentlyExecuting = false; - - this.registerCallback(); - }, - - registerCallback: function() { - this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); - }, - - execute: function() { - this.callback(this); - }, - - /** - * PeriodicalExecuter#stop() -> undefined - * - * Stops the periodical executer (there will be no further triggers). - **/ - stop: function() { - if (!this.timer) return; - clearInterval(this.timer); - this.timer = null; - }, - - onTimerEvent: function() { - if (!this.currentlyExecuting) { - try { - this.currentlyExecuting = true; - this.execute(); - } finally { - this.currentlyExecuting = false; - } - } - } -}); diff --git a/src/lang/range.js b/src/lang/range.js deleted file mode 100644 index 36501e1f7..000000000 --- a/src/lang/range.js +++ /dev/null @@ -1,55 +0,0 @@ -/** section: lang - * class ObjectRange -**/ - -/** section: lang - * $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. -**/ -function $R(start, end, exclusive) { - return new ObjectRange(start, end, exclusive); -} - -var ObjectRange = Class.create(Enumerable, (function() { - /** - * new ObjectRange(start, end[, exclusive = false]) - * - * TODO: new ObjectRange - **/ - function initialize(start, end, exclusive) { - this.start = start; - this.end = end; - this.exclusive = exclusive; - } - - function _each(iterator) { - var value = this.start; - while (this.include(value)) { - iterator(value); - value = value.succ(); - } - } - - /** - * ObjectRange#include(value) -> Boolean - * - * Determines whether the value is included in the range. - **/ - 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/string.js b/src/lang/string.js deleted file mode 100644 index 5530ebbab..000000000 --- a/src/lang/string.js +++ /dev/null @@ -1,444 +0,0 @@ -/** section: lang - * String -**/ -Object.extend(String, { - /** - * String.interpret(value) -> String - * - * TODO: String.interpret - **/ - 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) }; - } - - /** - * 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. - **/ - function gsub(pattern, replacement) { - var result = '', source = this, match; - replacement = prepareReplacement(replacement); - - if (!(pattern.length || pattern.source)) { - replacement = replacement(''); - return replacement + source.split('').join(replacement) + replacement; - } - - while (source.length > 0) { - if (match = source.match(pattern)) { - 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. - * The pattern can be a string or a regular expression. - **/ - 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. - **/ - 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). - **/ - 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. - **/ - function strip() { - return this.replace(/^\s+/, '').replace(/\s+$/, ''); - } - - /** - * String#stripTags() -> String - * - * Strips a string of any HTML tag. - **/ - function stripTags() { - return this.replace(/<\/?[^>]+>/gi, ''); - } - - /** - * String#stripScripts() -> String - * - * Strips a string of anything that looks like an HTML script block. - **/ - function stripScripts() { - return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), ''); - } - - /** - * String#extractScripts() -> Array - * - * Exctracts the content of any script block 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 script block present in the string. - * Returns an array containing the value returned by each script. - **/ - function evalScripts() { - return this.extractScripts().map(function(script) { return eval(script) }); - } - - /** - * String#escapeHTML() -> String - * - * Converts HTML special characters to their entity equivalents. - **/ - function escapeHTML() { - escapeHTML.text.data = this; - return escapeHTML.div.innerHTML; - } - - /** related to: String#escapeHTML - * String#unescapeHTML() -> String - * - * Strips tags and converts the entity forms of special HTML characters to their normal form. - **/ - function unescapeHTML() { - var div = new Element('div'); - div.innerHTML = this.stripTags(); - return div.childNodes[0] ? (div.childNodes.length > 1 ? - $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) : - div.childNodes[0].nodeValue) : ''; - } - - /** alias of: String#parseQuery, related to: Hash#toQueryString - * String#toQueryParams([separator = '&']) -> Object - * - * Parses a URI-like query string and returns an object composed of parameter/value pairs. - **/ - function toQueryParams(separator) { - var match = this.strip().match(/([^?#]*)(#.*)?$/); - if (!match) return { }; - - return match[1].split(separator || '&').inject({ }, function(hash, pair) { - if ((pair = pair.split('='))[0]) { - var key = decodeURIComponent(pair.shift()); - var value = pair.length > 1 ? pair.join('=') : pair[0]; - if (value != undefined) value = decodeURIComponent(value); - - if (key in hash) { - if (!Object.isArray(hash[key])) hash[key] = [hash[key]]; - hash[key].push(value); - } - else hash[key] = value; - } - return hash; - }); - } - - /** - * String#toArray() -> Array - * - * Splits the string character-by-character and returns an array with the result. - **/ - function toArray() { - return this.split(''); - } - - /** - * String#succ() -> String - * - * Used internally by ObjectRange. - * Converts the last character of the string to the following character in the - * Unicode alphabet. - **/ - function succ() { - return this.slice(0, this.length - 1) + - String.fromCharCode(this.charCodeAt(this.length - 1) + 1); - } - - /** - * String#times(count) -> String - * - * Concatenates the string `count` times. - **/ - function times(count) { - return count < 1 ? '' : new Array(count + 1).join(this); - } - - /** - * String#camelize() -> String - * - * Converts a string separated by dashes into a camelCase equivalent. - * For instance, 'foo-bar' would be converted to 'fooBar'. - **/ - function camelize() { - var parts = this.split('-'), len = parts.length; - if (len == 1) return parts[0]; - - var camelized = this.charAt(0) == '-' - ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1) - : parts[0]; - - for (var i = 1; i < len; i++) - camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1); - - return camelized; - } - - /** - * String#capitalize() -> String - * - * Capitalizes the first letter of a string and downcases all the others. - **/ - function capitalize() { - return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase(); - } - - /** - * String#underscore() -> String - * - * Converts a camelized string into a series of words separated by an underscore ("_"). - **/ - function underscore() { - return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase(); - } - - /** - * String#dasherize() -> String - * - * Replaces every instance of the underscore character ("_") by a dash ("-"). - **/ - function dasherize() { - return this.gsub(/_/,'-'); - } - - /** related to: Object.inspect - * String#inspect([useDoubleQuotes = false]) -> String - * - * Returns a debug-oriented version of the string (i.e. wrapped in single or - * double quotes, with backslashes and quotes escaped). - **/ - function inspect(useDoubleQuotes) { - var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) { - var character = String.specialChar[match[0]]; - return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16); - }); - if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"'; - return "'" + escapedString.replace(/'/g, '\\\'') + "'"; - } - - /** related to: Object.toJSON - * String#toJSON() -> String - * - * Returns a JSON string. - **/ - function toJSON() { - return this.inspect(true); - } - - /** - * String#unfilterJSON([filter = Prototype.JSONFilter]) -> String - * - * Strips comment delimiters around Ajax JSON or JavaScript responses. - * This security method is called internally. - **/ - function unfilterJSON(filter) { - return this.sub(filter || Prototype.JSONFilter, '#{1}'); - } - - /** - * String#isJSON() -> Boolean - * - * Check if the string is valid JSON by the use of regular expressions. - * This security method is called internally. - **/ - function isJSON() { - var str = this; - if (str.blank()) return false; - str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, ''); - return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str); - } - - /** - * String#evalJSON([sanitize = false]) -> object - * - * Evaluates the JSON in the string and returns the resulting object. - * If the optional sanitize parameter is set to true, the string is checked for - * possible malicious attempts and eval is not called if one is detected. - **/ - function evalJSON(sanitize) { - var json = this.unfilterJSON(); - try { - if (!sanitize || json.isJSON()) return eval('(' + json + ')'); - } catch (e) { } - throw new SyntaxError('Badly formed JSON string: ' + this.inspect()); - } - - /** - * String#include(substring) -> Boolean - * - * Check if the string contains a substring. - **/ - function include(pattern) { - return this.indexOf(pattern) > -1; - } - - /** - * String#startsWith(substring) -> Boolean - * - * Checks if the string starts with substring. - **/ - function startsWith(pattern) { - return this.indexOf(pattern) === 0; - } - - /** - * String#endsWith(substring) -> Boolean - * - * Checks if the string ends with substring. - **/ - function endsWith(pattern) { - var d = this.length - pattern.length; - return d >= 0 && this.lastIndexOf(pattern) === d; - } - - /** - * String#empty() -> Boolean - * - * Checks if the string is empty. - **/ - function empty() { - return this == ''; - } - - /** - * String#blank() -> Boolean - * - * Check if the string is 'blank', meaning either empty or containing only whitespace. - **/ - function blank() { - return /^\s*$/.test(this); - } - - /** - * String#interpolate(object[, pattern]) -> String - * - * Treats the string as a Template and fills it with object’s properties. - **/ - function interpolate(object, pattern) { - return new Template(this, pattern).evaluate(object); - } - - return { - gsub: gsub, - sub: sub, - scan: scan, - truncate: truncate, - strip: strip, - stripTags: stripTags, - stripScripts: stripScripts, - extractScripts: extractScripts, - evalScripts: evalScripts, - escapeHTML: escapeHTML, - unescapeHTML: unescapeHTML, - toQueryParams: toQueryParams, - parseQuery: toQueryParams, - toArray: toArray, - succ: succ, - times: times, - camelize: camelize, - capitalize: capitalize, - underscore: underscore, - dasherize: dasherize, - inspect: inspect, - toJSON: toJSON, - unfilterJSON: unfilterJSON, - isJSON: isJSON, - evalJSON: evalJSON, - include: include, - startsWith: startsWith, - endsWith: endsWith, - empty: empty, - blank: blank, - interpolate: interpolate - }; -})()); - -if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, { - escapeHTML: function() { - return this.replace(/&/g,'&').replace(//g,'>'); - }, - unescapeHTML: function() { - return this.stripTags().replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); - } -}); - -Object.extend(String.prototype.escapeHTML, { - div: document.createElement('div'), - text: document.createTextNode('') -}); - -String.prototype.escapeHTML.div.appendChild(String.prototype.escapeHTML.text); - diff --git a/src/lang/template.js b/src/lang/template.js deleted file mode 100644 index 6e8b743e4..000000000 --- a/src/lang/template.js +++ /dev/null @@ -1,48 +0,0 @@ -/** section: lang - * class Template -**/ -var Template = Class.create({ - /** - * new Template(template[, pattern = Template.Pattern]) - * - * TODO: new Template - **/ - initialize: function(template, pattern) { - this.template = template.toString(); - this.pattern = pattern || Template.Pattern; - }, - - /** - * Template#evaluate(object) -> String - * - * Applies the template to given `object`’s data, producing a formatted string - * with symbols replaced by corresponding object’s properties. - **/ - evaluate: function(object) { - if (Object.isFunction(object.toTemplateReplacements)) - object = object.toTemplateReplacements(); - - return this.template.gsub(this.pattern, function(match) { - if (object == null) return ''; - - var before = match[1] || ''; - if (before == '\\') return match[2]; - - var ctx = object, expr = match[3]; - var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/; - match = pattern.exec(expr); - if (match == null) return before; - - while (match != null) { - var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1]; - ctx = ctx[comp]; - if (null == ctx || '' == match[3]) break; - expr = expr.substring('[' == match[3] ? match[1].length : match[0].length); - match = pattern.exec(expr); - } - - return before + String.interpret(ctx); - }); - } -}); -Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; diff --git a/src/prototype.js b/src/prototype.js index 166e02da9..f493a7475 100644 --- a/src/prototype.js +++ b/src/prototype.js @@ -1,60 +1,6 @@ -/* Prototype JavaScript framework, version <%= PROTOTYPE_VERSION %> - * (c) 2005-2009 Sam Stephenson - * - * Prototype is freely distributable under the terms of an MIT-style license. - * For details, see the Prototype web site: http://www.prototypejs.org/ - * - *--------------------------------------------------------------------------*/ - -var Prototype = { - Version: '<%= PROTOTYPE_VERSION %>', - - Browser: { - IE: !!(window.attachEvent && - navigator.userAgent.indexOf('Opera') === -1), - Opera: navigator.userAgent.indexOf('Opera') > -1, - WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1, - Gecko: navigator.userAgent.indexOf('Gecko') > -1 && - navigator.userAgent.indexOf('KHTML') === -1, - MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/) - }, - - BrowserFeatures: { - XPath: !!document.evaluate, - SelectorsAPI: !!document.querySelector, - ElementExtensions: (function() { - if (window.HTMLElement && window.HTMLElement.prototype) - return true; - if (window.Element && window.Element.prototype) - return true; - })(), - SpecificElementExtensions: (function() { - // First, try the named class - if (typeof window.HTMLDivElement !== 'undefined') - return true; - - var div = document.createElement('div'); - if (div['__proto__'] && div['__proto__'] !== - document.createElement('form')['__proto__']) { - return true; - } - - return false; - })() - }, - - ScriptFragment: ']*>([\\S\\s]*?)<\/script>', - JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/, - - emptyFunction: function() { }, - K: function(x) { return x } -}; - -if (Prototype.Browser.MobileSafari) - Prototype.BrowserFeatures.SpecificElementExtensions = false; - -//= require "lang" -//= require "ajax" -//= require "dom" - -//= require "deprecated" +//= compat +//= require "./prototype/prototype" +//= require "./prototype/lang" +//= require "./prototype/ajax" +//= require "./prototype/dom" +//= require "./prototype/deprecated" diff --git a/src/prototype/ajax.js b/src/prototype/ajax.js new file mode 100644 index 000000000..abee64c6f --- /dev/null +++ b/src/prototype/ajax.js @@ -0,0 +1,129 @@ +//= compat +//= require "ajax/ajax" +//= require "ajax/responders" +//= require "ajax/base" +//= require "ajax/request" +//= require "ajax/response" +//= require "ajax/updater" +//= require "ajax/periodical_updater" + +/** + * == Ajax == + * + * Prototype's APIs around the `XmlHttpRequest` object. + * + * The Prototype framework enables you to deal with Ajax calls in a manner that + * is both easy and compatible with all modern browsers. + * + * Actual requests are made by creating instances of [[Ajax.Request]]. + * + * ##### Request headers + * + * The following headers are sent with all Ajax requests (and can be + * overridden with the `requestHeaders` option described below): + * + * * `X-Requested-With` is set to `XMLHttpRequest`. + * * `X-Prototype-Version` is set to Prototype's current version (e.g., + * `<%= PROTOTYPE_VERSION %>`). + * * `Accept` is set to `text/javascript, text/html, application/xml, + * text/xml, * / *` + * * `Content-type` is automatically determined based on the `contentType` + * and `encoding` options. + * + * ##### Ajax options + * + * All Ajax classes share a common set of _options_ and _callbacks_. + * Callbacks are called at various points in the life-cycle of a request, and + * always feature the same list of arguments. + * + * ##### Common options + * + * * `asynchronous` ([[Boolean]]; default `true`): Determines whether + * `XMLHttpRequest` is used asynchronously or not. Synchronous usage is + * **strongly discouraged** — it halts all script execution for the + * duration of the request _and_ blocks the browser UI. + * * `body` ([[String]]): Specific contents for the request body on a + * method that sends a payload (like `post` or `put`). If it is not + * provided, the contents of the `parameters` option will be used instead. + * * `contentType` ([[String]]; default `application/x-www-form-urlencoded`): + * The `Content-type` header for your request. Change this header if you + * want to send data in another format (like XML). + * * `encoding` ([[String]]; default `UTF-8`): The encoding for the contents + * of your request. It is best left as-is, but should weird encoding issues + * arise, you may have to tweak this. + * * `method` ([[String]]; default `post`): The HTTP method to use for the + * request. The other common possibility is `get`. Abiding by Rails + * conventions, Prototype also reacts to other HTTP verbs (such as `put` and + * `delete`) by submitting via `post` and adding a extra `_method` parameter + * with the originally-requested method. + * * `parameters` ([[String]]): The parameters for the request, which will be + * encoded into the URL for a `get` method, or into the request body for the + * other methods. This can be provided either as a URL-encoded string, a + * [[Hash]], or a plain [[Object]]. + * * `postBody` ([[String]]): An alias for `body` that is provided for + * backward-compatibility. Its use is discouraged. + * * `requestHeaders` ([[Object]]): A set of key-value pairs, with properties + * representing header names. + * * `evalJS` ([[Boolean]] | [[String]]; default `true`): Automatically `eval`s + * the content of [[Ajax.Response#responseText]] if the `Content-type` returned + * by the server is set to one of `text/javascript`, `application/ecmascript` + * (matches expression `(text|application)\/(x-)?(java|ecma)script`). + * If the request doesn't obey same-origin policy, the content is not evaluated. + * If you need to force evalutation, pass `'force'`. To prevent it altogether, + * pass `false`. + * * `evalJSON` ([[Boolean]] | [[String]]; default `true`): Automatically `eval`s + * the content of [[Ajax.Response#responseText]] and populates + * [[Ajax.Response#responseJSON]] with it if the `Content-type` returned by + * the server is set to `application/json`. If the request doesn't obey + * same-origin policy, the content is sanitized before evaluation. If you + * need to force evalutation, pass `'force'`. To prevent it altogether, pass + * `false`. + * * `sanitizeJSON` ([[Boolean]]; default is `false` for same-origin requests, + * `true` otherwise): Sanitizes the contents of + * [[Ajax.Response#responseText]] before evaluating it. + * + * ##### Common callbacks + * + * When used on individual instances, all callbacks (except `onException`) are + * invoked with two parameters: the [[Ajax.Response]] object and the result of + * evaluating the `X-JSON` response header, if any (can be `null`). + * + * For another way of describing their chronological order and which callbacks + * are mutually exclusive, see [[Ajax.Request]]. + * + * * `onCreate`: Triggered when the [[Ajax.Request]] object is initialized. + * This is _after_ the parameters and the URL have been processed, but + * _before_ opening the connection via the XHR object. + * * `onUninitialized` (*Not guaranteed*): Invoked just after the XHR object + * is created. + * * `onLoading` (*Not guaranteed*): Triggered when the underlying XHR object + * is being setup, and its connection opened. + * * `onLoaded` (*Not guaranteed*): Triggered once the underlying XHR object + * is setup, the connection is open, and it is ready to send its actual + * request. + * * `onInteractive` (*Not guaranteed*): Triggered whenever the requester + * receives a part of the response (but not the final part), should it + * be sent in several packets. + * * `onSuccess`: Invoked when a request completes and its status code is + * `undefined` or belongs in the `2xy` family. This is skipped if a + * code-specific callback is defined (e.g., `on200`), and happens _before_ + * `onComplete`. + * * `onFailure`: Invoked when a request completes and its status code exists + * but _is not_ in the `2xy` family. This is skipped if a code-specific + * callback is defined (e.g. `on403`), and happens _before_ `onComplete`. + * * `onXYZ` (_with `XYZ` representing any HTTP status code_): Invoked just + * after the response is complete _if_ the status code is the exact code + * used in the callback name. _Prevents_ execution of `onSuccess` and + * `onFailure`. Happens _before_ `onComplete`. + * * `onException`: Triggered whenever an XHR error arises. Has a custom + * signature: the first argument is the requester (i.e. an [[Ajax.Request]] + * instance), and the second is the exception object. The default + * `onException` handler simply re-throws the caught exception. (If you + * want exceptions to be silently ignored, pass in an empty function to + * `onException`.) + * * `onComplete`: Triggered at the _very end_ of a request's life-cycle, after + * the request completes, status-specific callbacks are called, and possible + * automatic behaviors are processed. Guaranteed to run regardless of what + * happened during the request. + * +**/ diff --git a/src/prototype/ajax/ajax.js b/src/prototype/ajax/ajax.js new file mode 100644 index 000000000..ab79023f4 --- /dev/null +++ b/src/prototype/ajax/ajax.js @@ -0,0 +1,17 @@ +/** section: Ajax + * Ajax +**/ + +var Ajax = { + getTransport: function() { + return new XMLHttpRequest(); + }, + + /** + * Ajax.activeRequestCount -> Number + * + * Represents the number of active XHR requests triggered through + * [[Ajax.Request]], [[Ajax.Updater]], or [[Ajax.PeriodicalUpdater]]. + **/ + activeRequestCount: 0 +}; diff --git a/src/ajax/base.js b/src/prototype/ajax/base.js similarity index 68% rename from src/ajax/base.js rename to src/prototype/ajax/base.js index b6413db8e..791cbcc0b 100644 --- a/src/ajax/base.js +++ b/src/prototype/ajax/base.js @@ -1,3 +1,4 @@ +// Abstract class; does not need documentation. Ajax.Base = Class.create({ initialize: function(options) { this.options = { @@ -7,15 +8,16 @@ Ajax.Base = Class.create({ encoding: 'UTF-8', parameters: '', evalJSON: true, - evalJS: true + evalJS: true, + onException: function (request, error) { + throw error; + } }; Object.extend(this.options, options || { }); - + this.options.method = this.options.method.toLowerCase(); - - if (Object.isString(this.options.parameters)) - this.options.parameters = this.options.parameters.toQueryParams(); - else if (Object.isHash(this.options.parameters)) + + if (Object.isHash(this.options.parameters)) this.options.parameters = this.options.parameters.toObject(); } }); diff --git a/src/prototype/ajax/periodical_updater.js b/src/prototype/ajax/periodical_updater.js new file mode 100644 index 000000000..5d2df01f9 --- /dev/null +++ b/src/prototype/ajax/periodical_updater.js @@ -0,0 +1,260 @@ +/** 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 [[Ajax.PeriodicalUpdater]] + * + * You can hit the brakes on a running [[Ajax.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]]. + * + * Periodically performs an AJAX request and updates a container's contents + * based on the response text. Offers a mechanism for "decay," which lets it + * trigger at widening intervals while the response is unchanged. + * + * This object addresses the common need of periodical update, which is used + * by all sorts of "polling" mechanisms (e.g. in an online chatroom or an + * online mail client). + * + * The basic idea is to run a regular [[Ajax.Updater]] at + * regular intervals, monitoring changes in the response text if the `decay` + * option (see below) is active. + * + * ##### Additional options + * + * [[Ajax.PeriodicalUpdater]] features all the common options and callbacks + * (see the [[Ajax section]] for more information), plus those added by + * [[Ajax.Updater]]. It also provides two new options that deal with the + * original period, and its decay rate (how Rocket Scientist does that make + * us sound, uh?!). + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
OptionDefaultDescription
frequency2Okay, this is not a frequency (e.g 0.5Hz), but a period (i.e. a number of seconds). + * Don't kill me, I didn't write this one! This is the minimum interval at which AJAX + * requests are made. You don't want to make it too short (otherwise you may very well + * end up with multiple requests in parallel, if they take longer to process and return), + * but you technically can provide a number below one, e.g. 0.75 second.
decay1This controls the rate at which the request interval grows when the response is + * unchanged. It is used as a multiplier on the current period (which starts at the original + * value of the frequency parameter). Every time a request returns an unchanged + * response text, the current period is multiplied by the decay. Therefore, the default + * value means regular requests (no change of interval). Values higher than one will + * yield growing intervals. Values below one are dangerous: the longer the response text + * stays the same, the more often you'll check, until the interval is so short your browser + * is left with no other choice than suicide. Note that, as soon as the response text + * does change, the current period resets to the original one.
+ * + * To better understand decay, here is a small sequence of calls from the + * following example: + * + * new Ajax.PeriodicalUpdater('items', '/items', { + * method: 'get', frequency: 3, decay: 2 + * }); + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Call#When?Decay beforeResponse changed?Decay afterNext periodComments
100:002n/a13Response is deemed changed, since there is no prior response to compare to!
200:031yes13Response did change again: we "reset" to 1, which was already the decay.
300:061no26Response didn't change: decay augments by the decay option factor: + * we're waiting longer now…
400:122no412Still no change, doubling again.
500:244no824Jesus, is this thing going to change or what?
600:488yes13Ah, finally! Resetting decay to 1, and therefore using the original period.
+ * + * ##### Disabling and re-enabling a [[Ajax.PeriodicalUpdater]] + * + * You can pull the brake on a running [[Ajax.PeriodicalUpdater]] by simply + * calling its `stop` method. If you wish to re-enable it later, just call + * its `start` method. Both take no argument. + * + * ##### Beware! Not a specialization! + * + * [[Ajax.PeriodicalUpdater]] is not a specialization of [[Ajax.Updater]], + * despite its name. When using it, do not expect to be able to use methods + * normally provided by [[Ajax.Request]] and "inherited" by [[Ajax.Updater]], + * such as `evalJSON` or `getHeader`. Also the `onComplete` callback is + * hijacked to be used for update management, so if you wish to be notified + * of every successful request, use `onSuccess` instead (beware: it will get + * called *before* the update is performed). + **/ + 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/prototype/ajax/request.js b/src/prototype/ajax/request.js new file mode 100644 index 000000000..f4ecaa16e --- /dev/null +++ b/src/prototype/ajax/request.js @@ -0,0 +1,361 @@ +/** section: Ajax + * class Ajax.Request + * + * Initiates and processes an Ajax request. + * + * [[Ajax.Request]] is a general-purpose class for making HTTP requests which + * handles the life-cycle of the request, handles the boilerplate, and lets + * you plug in callback functions for your custom needs. + * + * In the optional `options` hash, you usually provide an `onComplete` and/or + * `onSuccess` callback, unless you're in the edge case where you're getting a + * JavaScript-typed response, that will automatically be `eval`'d. + * + * For a full list of common options and callbacks, see "Ajax options" heading + * of the [[Ajax section]]. + * + * ##### A basic example + * + * new Ajax.Request('/your/url', { + * onSuccess: function(response) { + * // Handle the response content... + * } + * }); + * + * ##### Request life-cycle + * + * Underneath our nice requester objects lies, of course, `XMLHttpRequest`. The + * defined life-cycle is as follows: + * + * 1. Created + * 2. Initialized + * 3. Request sent + * 4. Response being received (can occur many times, as packets come in) + * 5. Response received, request complete + * + * As you can see under the "Ajax options" heading of the [[Ajax section]], + * Prototype's AJAX objects define a whole slew of callbacks, which are + * triggered in the following order: + * + * 1. `onCreate` (this is actually a callback reserved to [[Ajax.Responders]]) + * 2. `onUninitialized` (maps on Created) + * 3. `onLoading` (maps on Initialized) + * 4. `onLoaded` (maps on Request sent) + * 5. `onInteractive` (maps on Response being received) + * 6. `on`*XYZ* (numerical response status code), onSuccess or onFailure (see below) + * 7. `onComplete` + * + * The two last steps both map on *Response received*, in that order. If a + * status-specific callback is defined, it gets invoked. Otherwise, if + * `onSuccess` is defined and the response is deemed a success (see below), it + * is invoked. Otherwise, if `onFailure` is defined and the response is *not* + * deemed a success, it is invoked. Only after that potential first callback is + * `onComplete` called. + * + * ##### A note on portability + * + * Depending on how your browser implements `XMLHttpRequest`, one or more + * callbacks may never be invoked. In particular, `onLoaded` and + * `onInteractive` are not a 100% safe bet so far. However, the global + * `onCreate`, `onUninitialized` and the two final steps are very much + * guaranteed. + * + * ##### `onSuccess` and `onFailure`, the under-used callbacks + * + * Way too many people use [[Ajax.Request]] in a similar manner to raw XHR, + * defining only an `onComplete` callback even when they're only interested in + * "successful" responses, thereby testing it by hand: + * + * // This is too bad, there's better! + * new Ajax.Request('/your/url', { + * onComplete: function(response) { + * if (200 == response.status) + * // yada yada yada + * } + * }); + * + * First, as described below, you could use better "success" detection: success + * is generally defined, HTTP-wise, as either no response status or a "2xy" + * response status (e.g., 201 is a success, too). See the example below. + * + * Second, you could dispense with status testing altogether! Prototype adds + * callbacks specific to success and failure, which we listed above. Here's + * what you could do if you're only interested in success, for instance: + * + * new Ajax.Request('/your/url', { + * onSuccess: function(response) { + * // yada yada yada + * } + * }); + * + * ##### Automatic JavaScript response evaluation + * + * If an Ajax request follows the _same-origin policy_ **and** its response + * has a JavaScript-related `Content-type`, the content of the `responseText` + * property will automatically be passed to `eval`. + * + * In other words: you don't even need to provide a callback to leverage + * pure-JavaScript Ajax responses. This is the convention that drives Rails's + * RJS. + * + * The list of JavaScript-related MIME-types handled by Prototype is: + * + * * `application/ecmascript` + * * `application/javascript` + * * `application/x-ecmascript` + * * `application/x-javascript` + * * `text/ecmascript` + * * `text/javascript` + * * `text/x-ecmascript` + * * `text/x-javascript` + * + * The MIME-type string is examined in a case-insensitive manner. + * + * ##### Methods you may find useful + * + * Instances of the [[Ajax.Request]] object provide several methods that come + * in handy in your callback functions, especially once the request is complete. + * + * ###### Is the response a successful one? + * + * The [[Ajax.Request#success]] method examines the XHR object's `status` + * property and follows general HTTP guidelines: unknown status is deemed + * successful, as is the whole `2xy` status code family. It's a generally + * better way of testing your response than the usual + * `200 == transport.status`. + * + * ###### Getting HTTP response headers + * + * While you can obtain response headers from the XHR object using its + * `getResponseHeader` method, this makes for verbose code, and several + * implementations raise an exception when the header is not found. To make + * this easier, you can use the [[Ajax.Response#getHeader]] method, which + * delegates to the longer version and returns `null` if an exception occurs: + * + * new Ajax.Request('/your/url', { + * onSuccess: function(response) { + * // Note how we brace against null values + * if ((response.getHeader('Server') || '').match(/Apache/)) + * ++gApacheCount; + * // Remainder of the code + * } + * }); + * + * ##### Evaluating JSON headers + * + * Some backends will return JSON not as response text, but in the `X-JSON` + * header. In this case, you don't even need to evaluate the returned JSON + * yourself, as Prototype automatically does so. It passes the result as the + * `headerJSON` property of the [[Ajax.Response]] object. Note that if there + * is no such header — or its contents are invalid — `headerJSON` + * will be set to `null`. + * + * new Ajax.Request('/your/url', { + * onSuccess: function(transport) { + * transport.headerJSON + * } + * }); +**/ +Ajax.Request = Class.create(Ajax.Base, { + _complete: false, + + /** + * new Ajax.Request(url[, options]) + * - 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.Request`. + **/ + initialize: function($super, url, options) { + $super(options); + this.transport = Ajax.getTransport(); + this.request(url); + }, + + request: function(url) { + this.url = url; + this.method = this.options.method; + var params = Object.isString(this.options.parameters) ? + this.options.parameters : + Object.toQueryString(this.options.parameters); + + if (params && this.method === 'get') { + // when GET, append parameters to URL + this.url += (this.url.include('?') ? '&' : '?') + params; + } + + this.parameters = params.toQueryParams(); + + try { + var response = new Ajax.Response(this); + if (this.options.onCreate) this.options.onCreate(response); + Ajax.Responders.dispatch('onCreate', this, response); + + this.transport.open(this.method.toUpperCase(), this.url, + this.options.asynchronous); + + if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1); + + this.transport.onreadystatechange = this.onStateChange.bind(this); + this.setRequestHeaders(); + + if (this.method !== 'get') { + this.body = this.options.body || this.options.postBody || params; + } else { + this.body = null; + } + + this.transport.send(this.body); + + /* Force Firefox to handle ready state 4 for synchronous requests */ + if (!this.options.asynchronous && this.transport.overrideMimeType) + this.onStateChange(); + + } + catch (e) { + this.dispatchException(e); + } + }, + + onStateChange: function() { + var readyState = this.transport.readyState; + if (readyState > 1 && !((readyState == 4) && this._complete)) + this.respondToReadyState(this.transport.readyState); + }, + + setRequestHeaders: function() { + var headers = { + 'X-Requested-With': 'XMLHttpRequest', + 'X-Prototype-Version': Prototype.Version, + 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*' + }; + + if (this.method !== 'get') { + // Don't set the `Content-Type` header if the user has explicitly set + // `contentType` to anything falsy. + if (this.options.contentType) { + headers['Content-type'] = this.options.contentType + + (this.options.encoding ? '; charset=' + this.options.encoding : ''); + } + + /* Force "Connection: close" for older Mozilla browsers to work + * around a bug where XMLHttpRequest sends an incorrect + * Content-length header. See Mozilla Bugzilla #246651. + */ + if (this.transport.overrideMimeType && + (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005) + headers['Connection'] = 'close'; + } + + // user-defined headers + if (typeof this.options.requestHeaders == 'object') { + var extras = this.options.requestHeaders; + + if (Object.isFunction(extras.push)) + for (var i = 0, length = extras.length; i < length; i += 2) + headers[extras[i]] = extras[i+1]; + else + $H(extras).each(function(pair) { headers[pair.key] = pair.value }); + } + + // skip null or undefined values + for (var name in headers) + if (headers[name] != null) + this.transport.setRequestHeader(name, headers[name]); + }, + + /** + * Ajax.Request#success() -> Boolean + * + * Tests whether the request was successful. + **/ + success: function() { + var status = this.getStatus(); + return !status || (status >= 200 && status < 300) || status == 304; + }, + + getStatus: function() { + try { + // IE sometimes returns 1223 for a 204 response. + if (this.transport.status === 1223) return 204; + return this.transport.status || 0; + } catch (e) { return 0 } + }, + + respondToReadyState: function(readyState) { + var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this); + + if (state == 'Complete') { + try { + this._complete = true; + (this.options['on' + response.status] + || this.options['on' + (this.success() ? 'Success' : 'Failure')] + || Prototype.emptyFunction)(response, response.headerJSON); + } catch (e) { + this.dispatchException(e); + } + + var contentType = response.getHeader('Content-type'); + if (this.options.evalJS == 'force' + || (this.options.evalJS && this.isSameOrigin() && contentType + && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i))) + this.evalResponse(); + } + + try { + (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON); + } catch (e) { + this.dispatchException(e); + } finally { + Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON); + } + + if (state == 'Complete') { + // avoid memory leak in MSIE: clean up + this.transport.onreadystatechange = Prototype.emptyFunction; + } + }, + + isSameOrigin: function() { + var m = this.url.match(/^\s*https?:\/\/[^\/]*/); + return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({ + protocol: location.protocol, + domain: document.domain, + port: location.port ? ':' + location.port : '' + })); + }, + + /** + * Ajax.Request#getHeader(name) -> String | null + * - name (String): The name of an HTTP header that may have been part of + * the response. + * + * Returns the value of the given response header, or `null` if that header + * was not found. + **/ + getHeader: function(name) { + try { + return this.transport.getResponseHeader(name) || null; + } catch (e) { return null; } + }, + + evalResponse: function() { + try { + return eval((this.transport.responseText || '').unfilterJSON()); + } catch (e) { + this.dispatchException(e); + } + }, + + dispatchException: function(exception) { + (this.options.onException || Prototype.emptyFunction)(this, exception); + Ajax.Responders.dispatch('onException', this, exception); + } +}); + +Ajax.Request.Events = + ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; diff --git a/src/prototype/ajax/responders.js b/src/prototype/ajax/responders.js new file mode 100644 index 000000000..0f5611e37 --- /dev/null +++ b/src/prototype/ajax/responders.js @@ -0,0 +1,120 @@ +/** section: Ajax + * Ajax.Responders + * includes Enumerable + * + * A repository of global listeners notified about every step of + * Prototype-based Ajax requests. + * + * Sometimes, you need to provide generic behaviors over all Ajax operations + * happening on the page (through [[Ajax.Request]], [[Ajax.Updater]] or + * [[Ajax.PeriodicalUpdater]]). + * + * For instance, you might want to automatically show an indicator when an + * Ajax request is ongoing, and hide it when none are. You may well want to + * factor out exception handling as well, logging those somewhere on the page + * in a custom fashion. The possibilities are myriad. + * + * To achieve this, Prototype provides `Ajax.Responders`, which lets you + * register (and, if you wish, unregister later) _responders_, which are + * objects with specially-named methods. These names come from a set of + * general callbacks corresponding to different points in time (or outcomes) + * of an Ajax request's life cycle. + * + * For instance, Prototype automatically registers a responder that maintains + * a nifty variable: [[Ajax.activeRequestCount]]. This represents, at a given + * time, the number of currently active Ajax requests — by monitoring their + * `onCreate` and `onComplete` events. The code for this is fairly simple: + * + * Ajax.Responders.register({ + * onCreate: function() { + * Ajax.activeRequestCount++; + * }, + * onComplete: function() { + * Ajax.activeRequestCount--; + * } + * }); + * + * ##### Responder callbacks + * + * The callbacks for responders are similar to the callbacks described in + * the [[Ajax section]], but take a different signature. They're invoked with + * three parameters: the requester object (i.e., the corresponding "instance" + * of [[Ajax.Request]]), the `XMLHttpRequest` object, and the result of + * evaluating the `X-JSON` response header, if any (can be `null`). They also + * execute in the context of the responder, bound to the `this` reference. + * + * * `onCreate`: Triggered whenever a requester object from the `Ajax` + * namespace is created, after its parameters are adjusted and before its + * XHR connection is opened. This takes *two* arguments: the requester + * object and the underlying XHR object. + * * `onUninitialized` (*Not guaranteed*): Invoked just after the XHR object + * is created. + * * `onLoading` (*Not guaranteed*): Triggered when the underlying XHR object + * is being setup, and its connection opened. + * * `onLoaded` (*Not guaranteed*): Triggered once the underlying XHR object + * is setup, the connection is open, and it is ready to send its actual + * request. + * * `onInteractive` (*Not guaranteed*): Triggered whenever the requester + * receives a part of the response (but not the final part), should it + * be sent in several packets. + * * `onException`: Triggered whenever an XHR error arises. Has a custom + * signature: the first argument is the requester (i.e. an [[Ajax.Request]] + * instance), and the second is the exception object. + * * `onComplete`: Triggered at the _very end_ of a request's life-cycle, after + * the request completes, status-specific callbacks are called, and possible + * automatic behaviors are processed. Guaranteed to run regardless of what + * happened during the request. +**/ + +Ajax.Responders = { + responders: [], + + _each: function(iterator, context) { + this.responders._each(iterator, context); + }, + + /** + * Ajax.Responders.register(responder) -> undefined + * - responder (Object): A list of functions with keys corresponding to the + * names of possible callbacks. + * + * Add a group of responders to all Ajax requests. + **/ + register: function(responder) { + if (!this.include(responder)) + this.responders.push(responder); + }, + + /** + * Ajax.Responders.unregister(responder) -> undefined + * - responder (Object): A list of functions with keys corresponding to the + * names of possible callbacks. + * + * Remove a previously-added group of responders. + * + * As always, unregistering something requires you to use the very same + * object you used at registration. If you plan to use `unregister`, be sure + * to assign your responder to a _variable_ before passing it into + * [[Ajax.Responders#register]] — don't pass it an object literal. + **/ + unregister: function(responder) { + this.responders = this.responders.without(responder); + }, + + dispatch: function(callback, request, transport, json) { + this.each(function(responder) { + if (Object.isFunction(responder[callback])) { + try { + responder[callback].apply(responder, [request, transport, json]); + } catch (e) { } + } + }); + } +}; + +Object.extend(Ajax.Responders, Enumerable); + +Ajax.Responders.register({ + onCreate: function() { Ajax.activeRequestCount++ }, + onComplete: function() { Ajax.activeRequestCount-- } +}); diff --git a/src/prototype/ajax/response.js b/src/prototype/ajax/response.js new file mode 100644 index 000000000..c7b53bc04 --- /dev/null +++ b/src/prototype/ajax/response.js @@ -0,0 +1,192 @@ +/** section: Ajax + * class Ajax.Response + * + * A wrapper class around `XmlHttpRequest` for dealing with HTTP responses + * of Ajax requests. + * + * An instance of [[Ajax.Response]] is passed as the first argument of all Ajax + * requests' callbacks. You _will not_ need to create instances of + * [[Ajax.Response]] yourself. +**/ + +/** + * Ajax.Response#readyState -> Number + * + * A [[Number]] corresponding to the request's current state. + * + * `0` : `"Uninitialized"`
+ * `1` : `"Loading"`
+ * `2` : `"Loaded"`
+ * `3` : `"Interactive"`
+ * `4` : `"Complete"` +**/ + +/** + * Ajax.Response#responseText -> String + * + * The text body of the response. +**/ + +/** + * Ajax.Response#responseXML -> document | null + * + * The XML body of the response if the `Content-type` of the request is set + * to `application/xml`; `null` otherwise. +**/ + +/** + * Ajax.Response#responseJSON -> Object | Array | null + * + * The JSON body of the response if the `Content-type` of the request is set + * to `application/json`; `null` otherwise. +**/ + +/** + * Ajax.Response#headerJSON -> Object | Array | null + * + * Auto-evaluated content of the `X-JSON` header if present; `null` otherwise. + * This is useful to transfer _small_ amounts of data. +**/ + +/** + * Ajax.Response#request -> Ajax.Request | Ajax.Updater + * + * The request object itself (an instance of [[Ajax.Request]] or + * [[Ajax.Updater]]). +**/ + +/** + * Ajax.Response#transport -> XmlHttpRequest + * + * The native `XmlHttpRequest` object itself. +**/ + +Ajax.Response = Class.create({ + // Don't document the constructor; should never be manually instantiated. + initialize: function(request){ + this.request = request; + var transport = this.transport = request.transport, + readyState = this.readyState = transport.readyState; + + if ((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) { + this.status = this.getStatus(); + this.statusText = this.getStatusText(); + this.responseText = String.interpret(transport.responseText); + this.headerJSON = this._getHeaderJSON(); + } + + if (readyState == 4) { + var xml = transport.responseXML; + this.responseXML = Object.isUndefined(xml) ? null : xml; + this.responseJSON = this._getResponseJSON(); + } + }, + + /** + * Ajax.Response#status -> Number + * + * The HTTP status code sent by the server. + **/ + status: 0, + + /** + * Ajax.Response#statusText -> String + * + * The HTTP status text sent by the server. + **/ + statusText: '', + + getStatus: Ajax.Request.prototype.getStatus, + + getStatusText: function() { + try { + return this.transport.statusText || ''; + } catch (e) { return '' } + }, + + /** + * Ajax.Response#getHeader(name) -> String | null + * + * See [[Ajax.Request#getHeader]]. + **/ + getHeader: Ajax.Request.prototype.getHeader, + + /** + * Ajax.Response#getAllHeaders() -> String | null + * + * Returns a [[String]] containing all headers separated by line breaks. + * _Does not_ throw errors if no headers are present the way its native + * counterpart does. + **/ + getAllHeaders: function() { + try { + return this.getAllResponseHeaders(); + } catch (e) { return null } + }, + + /** + * Ajax.Response#getResponseHeader(name) -> String + * + * Returns the value of the requested header if present; throws an error + * otherwise. This is just a wrapper around the `XmlHttpRequest` method of + * the same name. Prefer it's shorter counterpart: + * [[Ajax.Response#getHeader]]. + **/ + getResponseHeader: function(name) { + return this.transport.getResponseHeader(name); + }, + + /** + * Ajax.Response#getAllResponseHeaders() -> String + * + * Returns a [[String]] containing all headers separated by line breaks; throws + * an error if no headers exist. This is just a wrapper around the + * `XmlHttpRequest` method of the same name. Prefer it's shorter counterpart: + * [[Ajax.Response#getAllHeaders]]. + **/ + getAllResponseHeaders: function() { + return this.transport.getAllResponseHeaders(); + }, + + _getHeaderJSON: function() { + var json = this.getHeader('X-JSON'); + if (!json) return null; + + try { + // Browsers expect HTTP headers to be ASCII and nothing else. Running + // them through `decodeURIComponent` processes them with the page's + // specified encoding. + json = decodeURIComponent(escape(json)); + } catch(e) { + // Except Chrome doesn't seem to need this, and calling + // `decodeURIComponent` on text that's already in the proper encoding + // will throw a `URIError`. The ugly solution is to assume that a + // `URIError` raised here signifies that the text is, in fact, already + // in the correct encoding, and treat the failure as a good sign. + // + // This is ugly, but so too is sending extended characters in an HTTP + // header with no spec to back you up. + } + + try { + return json.evalJSON(this.request.options.sanitizeJSON || + !this.request.isSameOrigin()); + } catch (e) { + this.request.dispatchException(e); + } + }, + + _getResponseJSON: function() { + var options = this.request.options; + if (!options.evalJSON || (options.evalJSON != 'force' && + !(this.getHeader('Content-type') || '').include('application/json')) || + this.responseText.blank()) + return null; + try { + return this.responseText.evalJSON(options.sanitizeJSON || + !this.request.isSameOrigin()); + } catch (e) { + this.request.dispatchException(e); + } + } +}); diff --git a/src/prototype/ajax/updater.js b/src/prototype/ajax/updater.js new file mode 100644 index 000000000..b69b78f79 --- /dev/null +++ b/src/prototype/ajax/updater.js @@ -0,0 +1,127 @@ +/** section: Ajax + * class Ajax.Updater < Ajax.Request + * + * A class that performs an Ajax request and updates a container's contents + * with the contents of the response. + * + * [[Ajax.Updater]] is a subclass of [[Ajax.Request]] built for a common + * use-case. + * + * ##### Example + * + * new Ajax.Updater('items', '/items', { + * parameters: { text: $F('text') } + * }); + * + * This example will make a request to the URL `/items` (with the given + * parameters); it will then replace the contents of the element with the ID + * of `items` with whatever response it receives. + * + * ##### Callbacks + * + * [[Ajax.Updater]] supports all the callbacks listed in the [[Ajax section]]. + * Note that the `onComplete` callback will be invoked **after** the element + * is updated. + * + * ##### Additional options + * + * [[Ajax.Updater]] has some options of its own apart from the common options + * described in the [[Ajax section]]: + * + * * `evalScripts` ([[Boolean]]; defaults to `false`): Whether `'); + * // -> Element (and prints "updated!" in an alert dialog). + * $('fruits').innerHTML; + * // -> '

Kiwi, banana and apple.

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

Melon, oranges and grapes.

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

Bar

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

Bar

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

Current settings:

+ *

+ * + * + *

+ *

Login options

+ *

+ * + * + *

+ *

+ * + * + *

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

Overview

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

Usage

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

Caching

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

Hidden elements

+ * + * Because it's a common case to want the dimensions of a hidden element + * (e.g., for animations), it's possible to measure elements that are + * hidden with `display: none`. + * + * However, **it's only possible to measure a hidden element if its parent + * is visible**. If its parent (or any other ancestor) is hidden, any + * width and height measurements will return `0`, as will measurements for + * `top|bottom|left|right`. + * + **/ + Element.Layout = Class.create(Hash, { + /** + * new Element.Layout(element[, preCompute = false]) + * - element (Element): The element to be measured. + * - preCompute (Boolean): Whether to compute all values at once. Default + * is `false`. + * + * Declare a new layout hash. + * + * The `preCompute` argument determines whether measurements will be + * lazy-loaded or not. If you plan to use many different measurements, + * it's often more performant to pre-compute, as it minimizes the + * amount of overhead needed to measure. If you need only one or two + * measurements, it's probably not worth it. + **/ + initialize: function($super, element, preCompute) { + $super(); + this.element = $(element); + + // nullify all properties keys + Element.Layout.PROPERTIES.each( function(property) { + this._set(property, null); + }, this); + + // The 'preCompute' boolean tells us whether we should fetch all values + // at once. If so, we should do setup/teardown only once. We set a flag + // so that we can ignore calls to `_begin` and `_end` elsewhere. + if (preCompute) { + this._preComputing = true; + this._begin(); + Element.Layout.PROPERTIES.each( this._compute, this ); + this._end(); + this._preComputing = false; + } + }, + + _set: function(property, value) { + return Hash.prototype.set.call(this, property, value); + }, + + // TODO: Investigate. + set: function(property, value) { + throw "Properties of Element.Layout are read-only."; + }, + + /** + * Element.Layout#get(property) -> Number + * - property (String): One of the properties defined in + * [[Element.Layout.PROPERTIES]]. + * + * Retrieve the measurement specified by `property`. Will throw an error + * if the property is invalid. + * + * ##### Caveats + * + * * `Element.Layout` can measure the dimensions of an element hidden with + * CSS (`display: none`), but _only_ if its parent element is visible. + **/ + get: function($super, property) { + // Try to fetch from the cache. + var value = $super(property); + return value === null ? this._compute(property) : value; + }, + + // `_begin` and `_end` are two functions that are called internally + // before and after any measurement is done. In certain conditions (e.g., + // when hidden), elements need a "preparation" phase that ensures + // accuracy of measurements. + _begin: function() { + if (this._isPrepared()) return; + + var element = this.element; + if (isDisplayed(element)) { + this._setPrepared(true); + return; + } + + // If we get this far, it means this element is hidden. To get usable + // measurements, we must remove `display: none`, but in a manner that + // isn't noticeable to the user. That means we also set + // `visibility: hidden` to make it invisible, and `position: absolute` + // so that it won't alter the document flow when displayed. + // + // Once we do this, the element is "prepared," and we can make our + // measurements. When we're done, the `_end` method cleans up our + // changes. + + // Remember the original values for some styles we're going to alter. + var originalStyles = { + position: element.style.position || '', + width: element.style.width || '', + visibility: element.style.visibility || '', + display: element.style.display || '' + }; + + // We store them so that the `_end` method can retrieve them later. + element.store('prototype_original_styles', originalStyles); + + var position = getRawStyle(element, 'position'), width = element.offsetWidth; + + if (width === 0 || width === null) { + // Opera/IE won't report the true width of the element through + // `getComputedStyle` if it's hidden. If we got a nonsensical value, + // we need to show the element and try again. + element.style.display = 'block'; + width = element.offsetWidth; + } + + // Preserve the context in case we get a percentage value. + var context = (position === 'fixed') ? document.viewport : + element.parentNode; + + var tempStyles = { + visibility: 'hidden', + display: 'block' + }; + + // If the element's `position: fixed`, it's already out of the document + // flow, so it's both unnecessary and inaccurate to set + // `position: absolute`. + if (position !== 'fixed') tempStyles.position = 'absolute'; + + element.setStyle(tempStyles); + + var positionedWidth = element.offsetWidth, newWidth; + if (width && (positionedWidth === width)) { + // If the element's width is the same both before and after + // we set absolute positioning, that means: + // (a) it was already absolutely-positioned; or + // (b) it has an explicitly-set width, instead of width: auto. + // Either way, it means the element is the width it needs to be + // in order to report an accurate height. + newWidth = getContentWidth(element, context); + } else if (position === 'absolute' || position === 'fixed') { + // Absolute- and fixed-position elements' dimensions don't depend + // upon those of their parents. + newWidth = getContentWidth(element, context); + } else { + // Otherwise, the element's width depends upon the width of its + // parent. + var parent = element.parentNode, pLayout = parent.getLayout(); + + newWidth = pLayout.get('width') - + this.get('margin-left') - + this.get('border-left') - + this.get('padding-left') - + this.get('padding-right') - + this.get('border-right') - + this.get('margin-right'); + } + + // Whatever the case, we've now figured out the correct `width` value + // for the element. + element.setStyle({ width: newWidth + 'px' }); + + // The element is now ready for measuring. + this._setPrepared(true); + }, + + _end: function() { + var element = this.element; + var originalStyles = element.retrieve('prototype_original_styles'); + element.store('prototype_original_styles', null); + element.setStyle(originalStyles); + this._setPrepared(false); + }, + + _compute: function(property) { + var COMPUTATIONS = Element.Layout.COMPUTATIONS; + if (!(property in COMPUTATIONS)) { + throw "Property not found."; + } + + return this._set(property, COMPUTATIONS[property].call(this, this.element)); + }, + + _isPrepared: function() { + return this.element.retrieve('prototype_element_layout_prepared', false); + }, + + _setPrepared: function(bool) { + return this.element.store('prototype_element_layout_prepared', bool); + }, + + /** + * Element.Layout#toObject([keys...]) -> Object + * - keys (String): A space-separated list of keys to include. + * + * Converts the layout hash to a plain object of key/value pairs, + * optionally including only the given keys. + * + * Keys can be passed into this method as individual arguments _or_ + * separated by spaces within a string. + * + * // Equivalent statements: + * someLayout.toObject('top', 'bottom', 'left', 'right'); + * someLayout.toObject('top bottom left right'); + **/ + toObject: function() { + var args = $A(arguments); + var keys = (args.length === 0) ? Element.Layout.PROPERTIES : + args.join(' ').split(' '); + var obj = {}; + keys.each( function(key) { + // Key needs to be a valid Element.Layout property. + if (!Element.Layout.PROPERTIES.include(key)) return; + var value = this.get(key); + if (value != null) obj[key] = value; + }, this); + return obj; + }, + + /** + * Element.Layout#toHash([keys...]) -> Hash + * - keys (String): A space-separated list of keys to include. + * + * Converts the layout hash to an ordinary hash of key/value pairs, + * optionally including only the given keys. + * + * Keys can be passed into this method as individual arguments _or_ + * separated by spaces within a string. + * + * // Equivalent statements: + * someLayout.toHash('top', 'bottom', 'left', 'right'); + * someLayout.toHash('top bottom left right'); + **/ + toHash: function() { + var obj = this.toObject.apply(this, arguments); + return new Hash(obj); + }, + + /** + * Element.Layout#toCSS([keys...]) -> Object + * - keys (String): A space-separated list of keys to include. + * + * Converts the layout hash to a plain object of CSS property/value + * pairs, optionally including only the given keys. + * + * Keys can be passed into this method as individual arguments _or_ + * separated by spaces within a string. + * + * // Equivalent statements: + * someLayout.toCSS('top', 'bottom', 'left', 'right'); + * someLayout.toCSS('top bottom left right'); + * + * Useful for passing layout properties to [[Element.setStyle]]. + **/ + toCSS: function() { + var args = $A(arguments); + var keys = (args.length === 0) ? Element.Layout.PROPERTIES : + args.join(' ').split(' '); + var css = {}; + + keys.each( function(key) { + // Key needs to be a valid Element.Layout property... + if (!Element.Layout.PROPERTIES.include(key)) return; + // ...but not a composite property. + if (Element.Layout.COMPOSITE_PROPERTIES.include(key)) return; + + var value = this.get(key); + if (value != null) css[cssNameFor(key)] = value + 'px'; + }, this); + return css; + }, + + inspect: function() { + return "#"; + } + }); + + Object.extend(Element.Layout, { + /** + * Element.Layout.PROPERTIES = Array + * + * A list of all measurable properties. + **/ + PROPERTIES: $w('height width top left right bottom border-left border-right border-top border-bottom padding-left padding-right padding-top padding-bottom margin-top margin-bottom margin-left margin-right padding-box-width padding-box-height border-box-width border-box-height margin-box-width margin-box-height'), + + /** + * Element.Layout.COMPOSITE_PROPERTIES = Array + * + * A list of all composite properties. Composite properties don't map + * directly to CSS properties — they're combinations of other + * properties. + **/ + COMPOSITE_PROPERTIES: $w('padding-box-width padding-box-height margin-box-width margin-box-height border-box-width border-box-height'), + + COMPUTATIONS: { + 'height': function(element) { + if (!this._preComputing) this._begin(); + + var bHeight = this.get('border-box-height'); + if (bHeight <= 0) { + if (!this._preComputing) this._end(); + return 0; + } + + var bTop = this.get('border-top'), + bBottom = this.get('border-bottom'); + + var pTop = this.get('padding-top'), + pBottom = this.get('padding-bottom'); + + if (!this._preComputing) this._end(); + + return bHeight - bTop - bBottom - pTop - pBottom; + }, + + 'width': function(element) { + if (!this._preComputing) this._begin(); + + var bWidth = this.get('border-box-width'); + if (bWidth <= 0) { + if (!this._preComputing) this._end(); + return 0; + } + + var bLeft = this.get('border-left'), + bRight = this.get('border-right'); + + var pLeft = this.get('padding-left'), + pRight = this.get('padding-right'); + + if (!this._preComputing) this._end(); + return bWidth - bLeft - bRight - pLeft - pRight; + }, + + 'padding-box-height': function(element) { + var height = this.get('height'), + pTop = this.get('padding-top'), + pBottom = this.get('padding-bottom'); + + return height + pTop + pBottom; + }, + + 'padding-box-width': function(element) { + var width = this.get('width'), + pLeft = this.get('padding-left'), + pRight = this.get('padding-right'); + + return width + pLeft + pRight; + }, + + 'border-box-height': function(element) { + if (!this._preComputing) this._begin(); + var height = element.offsetHeight; + if (!this._preComputing) this._end(); + return height; + }, + + 'border-box-width': function(element) { + if (!this._preComputing) this._begin(); + var width = element.offsetWidth; + if (!this._preComputing) this._end(); + return width; + }, + + 'margin-box-height': function(element) { + var bHeight = this.get('border-box-height'), + mTop = this.get('margin-top'), + mBottom = this.get('margin-bottom'); + + if (bHeight <= 0) return 0; + + return bHeight + mTop + mBottom; + }, + + 'margin-box-width': function(element) { + var bWidth = this.get('border-box-width'), + mLeft = this.get('margin-left'), + mRight = this.get('margin-right'); + + if (bWidth <= 0) return 0; + + return bWidth + mLeft + mRight; + }, + + 'top': function(element) { + var offset = element.positionedOffset(); + return offset.top; + }, + + 'bottom': function(element) { + var parent = element.getOffsetParent(); + var rect = element.getBoundingClientRect(), + pRect = parent.getBoundingClientRect(); + + return (pRect.bottom - rect.bottom).round(); + }, + + 'left': function(element) { + var offset = element.positionedOffset(); + return offset.left; + }, + + 'right': function(element) { + var parent = element.getOffsetParent(); + var rect = element.getBoundingClientRect(), + pRect = parent.getBoundingClientRect(); + + return (pRect.right - rect.right).round(); + }, + + 'padding-top': function(element) { + return getPixelValue(element, 'paddingTop'); + }, + + 'padding-bottom': function(element) { + return getPixelValue(element, 'paddingBottom'); + }, + + 'padding-left': function(element) { + return getPixelValue(element, 'paddingLeft'); + }, + + 'padding-right': function(element) { + return getPixelValue(element, 'paddingRight'); + }, + + 'border-top': function(element) { + return getPixelValue(element, 'borderTopWidth'); + }, + + 'border-bottom': function(element) { + return getPixelValue(element, 'borderBottomWidth'); + }, + + 'border-left': function(element) { + return getPixelValue(element, 'borderLeftWidth'); + }, + + 'border-right': function(element) { + return getPixelValue(element, 'borderRightWidth'); + }, + + 'margin-top': function(element) { + return getPixelValue(element, 'marginTop'); + }, + + 'margin-bottom': function(element) { + return getPixelValue(element, 'marginBottom'); + }, + + 'margin-left': function(element) { + return getPixelValue(element, 'marginLeft'); + }, + + 'margin-right': function(element) { + return getPixelValue(element, 'marginRight'); + } + } + }); + + /** + * class Element.Offset + * + * A representation of the top- and left-offsets of an element relative to + * another. + * + * All methods that compute offsets return an instance of `Element.Offset`. + * + **/ + Element.Offset = Class.create({ + /** + * new Element.Offset(left, top) + * + * Instantiates an [[Element.Offset]]. You shouldn't need to call this + * directly. + **/ + initialize: function(left, top) { + this.left = left.round(); + this.top = top.round(); + + // Act like an array. + this[0] = this.left; + this[1] = this.top; + }, + + /** + * Element.Offset#relativeTo(offset) -> Element.Offset + * - offset (Element.Offset): Another offset to compare to. + * + * Returns a new [[Element.Offset]] with its origin at the given + * `offset`. Useful for determining an element's distance from another + * arbitrary element. + **/ + relativeTo: function(offset) { + return new Element.Offset( + this.left - offset.left, + this.top - offset.top + ); + }, + + /** + * Element.Offset#inspect() -> String + * + * Returns a debug-friendly representation of the offset. + **/ + inspect: function() { + return "#".interpolate(this); + }, + + /** + * Element.Offset#toString() -> String + **/ + toString: function() { + return "[#{left}, #{top}]".interpolate(this); + }, + + /** + * Element.Offset#toArray() -> Array + * + * Returns an array representation fo the offset in [x, y] format. + **/ + toArray: function() { + return [this.left, this.top]; + } + }); + + /** + * Element.getLayout(@element[, preCompute = false]) -> Element.Layout + * - element (Element): The element to be measured. + * - preCompute (Boolean): Whether to compute all values at once. Default + * is `false`. + * + * Returns an instance of [[Element.Layout]] for measuring an element's + * dimensions. + * + * Note that this method returns a _new_ `Element.Layout` object each time + * it's called. If you want to take advantage of measurement caching, + * retain a reference to one `Element.Layout` object, rather than calling + * `Element.getLayout` whenever you need a measurement. You should call + * `Element.getLayout` again only when the values in an existing + * `Element.Layout` object have become outdated. + * + * If the `preCompute` argument is `true`, all properties will be measured + * when the layout object is instantiated. If you plan to measure several + * properties of an element's dimensions, it's probably worth it to get a + * pre-computed hash. + * + * ##### Examples + * + * var layout = $('troz').getLayout(); + * + * layout.get('width'); //-> 150 + * layout.get('height'); //-> 500 + * layout.get('padding-left'); //-> 10 + * layout.get('margin-left'); //-> 25 + * layout.get('border-top'); //-> 5 + * layout.get('border-bottom'); //-> 5 + * + * // Won't re-compute width; remembers value from first time. + * layout.get('width'); //-> 150 + * + * // Composite values obtained by adding together other properties; + * // will re-use any values we've already looked up above. + * layout.get('padding-box-width'); //-> 170 + * layout.get('border-box-height'); //-> 510 + * + * ##### Caveats + * + * * Instances of `Element.Layout` can measure the dimensions of an + * element hidden with CSS (`display: none`), but _only_ if its parent + * element is visible. + **/ + function getLayout(element, preCompute) { + return new Element.Layout(element, preCompute); + } + + /** + * Element.measure(@element, property) -> Number + * + * Gives the pixel value of `element`'s dimension specified by + * `property`. + * + * Useful for one-off measurements of elements. If you find yourself + * calling this method frequently over short spans of code, you might want + * to call [[Element.getLayout]] and operate on the [[Element.Layout]] + * object itself (thereby taking advantage of measurement caching). + * + * ##### Examples + * + * $('troz').measure('width'); //-> 150 + * $('troz').measure('border-top'); //-> 5 + * $('troz').measure('top'); //-> 226 + * + * ##### Caveats + * + * * `Element.measure` can measure the dimensions of an element hidden with + * CSS (`display: none`), but _only_ if its parent element is visible. + **/ + function measure(element, property) { + return $(element).getLayout().get(property); + } + + /** + * Element.getHeight(@element) -> Number + * + * Returns the height of `element`. + * + * This method returns correct values on elements whose display is set to + * `none` either in an inline style rule or in an CSS stylesheet. + * + * For performance reasons, if you need to query both width _and_ height of + * `element`, you should consider using [[Element.getDimensions]] instead. + * + * Note that the value returned is a _number only_ although it is + * _expressed in pixels_. + * + * ##### Examples + * + * language: html + *
+ * + * Then: + * + * $('rectangle').getHeight(); + * // -> 100 + **/ + function getHeight(element) { + return Element.getDimensions(element).height; + } + + /** + * Element.getWidth(@element) -> Number + * + * Returns the width of `element`. + * + * This method returns correct values on elements whose display is set to + * `none` either in an inline style rule or in an CSS stylesheet. + * + * For performance reasons, if you need to query both width _and_ height of + * `element`, you should consider using [[Element.getDimensions]] instead. + * + * Note that the value returned is a _number only_ although it is + * _expressed in pixels_. + * + * ##### Examples + * + * language: html + *
+ * + * Then: + * + * $('rectangle').getWidth(); + * // -> 200 + **/ + function getWidth(element) { + return Element.getDimensions(element).width; + } + + /** + * Element.getDimensions(@element) -> Object + * + * Finds the computed width and height of `element` and returns them as + * key/value pairs of an object. + * + * For backwards-compatibility, these dimensions represent the dimensions + * of the element's "border box" (including CSS padding and border). This + * is equivalent to the built-in `offsetWidth` and `offsetHeight` + * browser properties. + * + * Note that all values are returned as _numbers only_ although they are + * _expressed in pixels_. + * + * ##### Caveats + * + * * If the element is hidden via `display: none` in CSS, this method will + * attempt to measure the element by temporarily removing that CSS and + * applying `visibility: hidden` and `position: absolute`. This gives + * the element dimensions without making it visible or affecting the + * positioning of surrounding elements — but may not give accurate + * results in some cases. [[Element.measure]] is designed to give more + * accurate results. + * + * * In order to avoid calling the method twice, you should consider + * caching the returned values in a variable, as shown in the example + * below. + * + * * For more complex use cases, use [[Element.measure]], which is able + * to measure many different aspects of an element's dimensions and + * offsets. + * + * ##### Examples + * + * language: html + *
+ * + * Then: + * + * var dimensions = $('rectangle').getDimensions(); + * // -> {width: 200, height: 100} + * + * dimensions.width; + * // -> 200 + * + * dimensions.height; + * // -> 100 + **/ + function getDimensions(element) { + element = $(element); + var display = Element.getStyle(element, 'display'); + + if (display && display !== 'none') { + return { width: element.offsetWidth, height: element.offsetHeight }; + } + + // All *Width and *Height properties give 0 on elements with + // `display: none`, so show the element temporarily. + var style = element.style; + var originalStyles = { + visibility: style.visibility, + position: style.position, + display: style.display + }; + + var newStyles = { + visibility: 'hidden', + display: 'block' + }; + + // Switching `fixed` to `absolute` causes issues in Safari. + if (originalStyles.position !== 'fixed') + newStyles.position = 'absolute'; + + Element.setStyle(element, newStyles); + + var dimensions = { + width: element.offsetWidth, + height: element.offsetHeight + }; + + Element.setStyle(element, originalStyles); + + return dimensions; + } + + /** + * Element.getOffsetParent(@element) -> Element + * + * Returns `element`'s closest _positioned_ ancestor. If none is found, the + * `body` element is returned. + **/ + function getOffsetParent(element) { + element = $(element); + + // Ensure we never return the root HTML tag. + function selfOrBody(element) { + return isHtml(element) ? document.body : $(element); + } + + // For unusual cases like these, we standardize on returning the BODY + // element as the offset parent. + if (isDocument(element) || isDetached(element) || isBody(element) || isHtml(element)) + return document.body; + + // IE reports offset parent incorrectly for inline elements. + var isInline = (Element.getStyle(element, 'display') === 'inline'); + if (!isInline && element.offsetParent) return selfOrBody(element.offsetParent); + + while ((element = element.parentNode) && element !== document.body) { + if (Element.getStyle(element, 'position') !== 'static') { + return selfOrBody(element); + } + } + + return document.body; + } + + + /** + * Element.cumulativeOffset(@element) -> Element.Offset + * + * Returns the offsets of `element` from the top left corner of the + * document. + **/ + function cumulativeOffset(element) { + element = $(element); + var valueT = 0, valueL = 0; + if (element.parentNode) { + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + } + return new Element.Offset(valueL, valueT); + } + + /** + * Element.positionedOffset(@element) -> Element.Offset + * + * Returns `element`'s offset relative to its closest positioned ancestor + * (the element that would be returned by [[Element.getOffsetParent]]). + **/ + function positionedOffset(element) { + element = $(element); + + // Account for the margin of the element. + var layout = element.getLayout(); + + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + if (element) { + if (isBody(element)) break; + var p = Element.getStyle(element, 'position'); + if (p !== 'static') break; + } + } while (element); + + valueL -= layout.get('margin-left'); + valueT -= layout.get('margin-top'); + + return new Element.Offset(valueL, valueT); + } + + /** + * Element.cumulativeScrollOffset(@element) -> Element.Offset + * + * Calculates the cumulative scroll offset of an element in nested + * scrolling containers. + **/ + function cumulativeScrollOffset(element) { + var valueT = 0, valueL = 0; + do { + if (element === document.body) { + var bodyScrollNode = document.documentElement || document.body.parentNode || document.body; + valueT += !Object.isUndefined(window.pageYOffset) ? window.pageYOffset : bodyScrollNode.scrollTop || 0; + valueL += !Object.isUndefined(window.pageXOffset) ? window.pageXOffset : bodyScrollNode.scrollLeft || 0; + break; + } else { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } + } while (element); + return new Element.Offset(valueL, valueT); + } + + /** + * Element.viewportOffset(@element) -> Element.Offset + * + * Returns the X/Y coordinates of element relative to the viewport. + **/ + function viewportOffset(element) { + element = $(element); + if (isDetached(element)) return new Element.Offset(0, 0); + + var rect = element.getBoundingClientRect(); + return new Element.Offset(rect.left, rect.top); + } + + /** + * Element.absolutize(@element) -> Element + * + * Turns `element` into an absolutely-positioned element _without_ + * changing its position in the page layout. + **/ + function absolutize(element) { + element = $(element); + + if (Element.getStyle(element, 'position') === 'absolute') { + return element; + } + + var offsetParent = getOffsetParent(element); + var eOffset = element.viewportOffset(), + pOffset = offsetParent.viewportOffset(); + + var offset = eOffset.relativeTo(pOffset); + var layout = element.getLayout(); + + element.store('prototype_absolutize_original_styles', { + position: element.getStyle('position'), + left: element.getStyle('left'), + top: element.getStyle('top'), + width: element.getStyle('width'), + height: element.getStyle('height') + }); + + element.setStyle({ + position: 'absolute', + top: offset.top + 'px', + left: offset.left + 'px', + width: layout.get('width') + 'px', + height: layout.get('height') + 'px' + }); + + return element; + } + + /** + * Element.relativize(@element) -> Element + * + * Turns `element` into a relatively-positioned element without changing + * its position in the page layout. + * + * Used to undo a call to [[Element.absolutize]]. + **/ + function relativize(element) { + element = $(element); + if (Element.getStyle(element, 'position') === 'relative') { + return element; + } + + // Restore the original styles as captured by Element#absolutize. + var originalStyles = + element.retrieve('prototype_absolutize_original_styles'); + + if (originalStyles) element.setStyle(originalStyles); + return element; + } + + + /** + * Element.scrollTo(@element) -> Element + * + * Scrolls the window so that `element` appears at the top of the viewport. + * + * This has a similar effect than what would be achieved using + * [HTML anchors](http://www.w3.org/TR/html401/struct/links.html#h-12.2.3) + * (except the browser's history is not modified). + * + * ##### Example + * + * $(element).scrollTo(); + * // -> Element + **/ + function scrollTo(element) { + element = $(element); + var pos = Element.cumulativeOffset(element); + window.scrollTo(pos.left, pos.top); + return element; + } + + + /** + * Element.makePositioned(@element) -> Element + * + * Allows for the easy creation of a CSS containing block by setting + * `element`'s CSS `position` to `relative` if its initial position is + * either `static` or `undefined`. + * + * To revert back to `element`'s original CSS position, use + * [[Element.undoPositioned]]. + **/ + function makePositioned(element) { + element = $(element); + var position = Element.getStyle(element, 'position'), styles = {}; + if (position === 'static' || !position) { + styles.position = 'relative'; + Element.setStyle(element, styles); + Element.store(element, 'prototype_made_positioned', true); + } + return element; + } + + /** + * Element.undoPositioned(@element) -> Element + * + * Sets `element` back to the state it was in _before_ + * [[Element.makePositioned]] was applied to it. + * + * `element`'s absolutely positioned children will now have their positions + * set relatively to `element`'s nearest ancestor with a CSS `position` of + * `'absolute'`, `'relative'` or `'fixed'`. + **/ + function undoPositioned(element) { + element = $(element); + var storage = Element.getStorage(element), + madePositioned = storage.get('prototype_made_positioned'); + + if (madePositioned) { + storage.unset('prototype_made_positioned'); + Element.setStyle(element, { + position: '', + top: '', + bottom: '', + left: '', + right: '' + }); + } + return element; + } + + /** + * Element.makeClipping(@element) -> Element + * + * Simulates the poorly-supported CSS `clip` property by setting `element`'s + * `overflow` value to `hidden`. + * + * To undo clipping, use [[Element.undoClipping]]. + * + * The visible area is determined by `element`'s width and height. + * + * ##### Example + * + * language:html + *
+ * example + *
+ * + * Then: + * + * $('framer').makeClipping().setStyle({width: '100px', height: '100px'}); + * // -> Element + * + * Another example: + * + * language: html + * Click me to try it out. + * + *
+ * example + *
+ * + * + **/ + function makeClipping(element) { + element = $(element); + + var storage = Element.getStorage(element), + madeClipping = storage.get('prototype_made_clipping'); + + // The "prototype_made_clipping" storage key is meant to hold the + // original CSS overflow value. A string value or `null` means that we've + // called `makeClipping` already. An `undefined` value means we haven't. + if (Object.isUndefined(madeClipping)) { + var overflow = Element.getStyle(element, 'overflow'); + storage.set('prototype_made_clipping', overflow); + if (overflow !== 'hidden') + element.style.overflow = 'hidden'; + } + + return element; + } + + /** + * Element.undoClipping(@element) -> Element + * + * Sets `element`'s CSS `overflow` property back to the value it had + * _before_ [[Element.makeClipping]] was applied. + * + * ##### Example + * + * language: html + *
+ * example + *
+ * + * Then: + * + * $('framer').undoClipping(); + * // -> Element (and sets the CSS overflow property to its original value). + * + * Another example: + * + * language: html + * Click me to try it out. + * + *
+ * example + *
+ * + * + **/ + function undoClipping(element) { + element = $(element); + var storage = Element.getStorage(element), + overflow = storage.get('prototype_made_clipping'); + + if (!Object.isUndefined(overflow)) { + storage.unset('prototype_made_clipping'); + element.style.overflow = overflow || ''; + } + + return element; + } + + /** + * Element.clonePosition(@element, source[, options]) -> Element + * - source (Element | String): The source element (or its ID). + * - options (Object): The position fields to clone. + * + * Clones the position and/or dimensions of `source` onto the element as + * defined by `options`, with an optional offset for the `left` and `top` + * properties. + * + * Note that the element will be positioned exactly like `source` whether or + * not it is part of the same [CSS containing + * block](http://www.w3.org/TR/CSS21/visudet.html#containing-block-details). + * + * Also note that `element` must already be `position: absolute` or + * `position: fixed`. This method will not apply a `position` style. + * + * ##### Options + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
NameDefaultDescription
setLefttrueClones source's left CSS property onto element.
setToptrueClones source's top CSS property onto element.
setWidthtrueClones source's width onto element.
setHeighttrueClones source's width onto element.
offsetLeft0Number by which to offset element's left CSS property.
offsetTop0Number by which to offset element's top CSS property.
+ **/ + function clonePosition(element, source, options) { + options = Object.extend({ + setLeft: true, + setTop: true, + setWidth: true, + setHeight: true, + offsetTop: 0, + offsetLeft: 0 + }, options || {}); + + // Find page position of source. + source = $(source); + element = $(element); + var p, delta, layout, styles = {}; + + var isAbsolute = Element.getStyle(element, 'position') === 'absolute'; + var parent = Element.getOffsetParent(element); + + if (options.setLeft || options.setTop) { + // We start by measuring the source's viewport offset. + p = Element.viewportOffset(source); + + // If the element we're altering is `position: fixed`, that's all the + // information we need: later we'll apply that offset to the `top` and + // `left` properties directly. + delta = [0, 0]; + + // But if it's `position: absolute`, we have to know where its offset + // parent is positioned and take those measurements into account as + // well. + if (isAbsolute && parent !== document.body) { + delta = Element.viewportOffset(parent); + } + } + + function pageScrollXY() { + var x = 0, y = 0; + if (Object.isNumber(window.pageXOffset)) { + // Modern browsers. + x = window.pageXOffset; + y = window.pageYOffset; + } else if (document.body && (document.body.scrollLeft || document.body.scrollTop)) { + x = document.body.scrollLeft; + y = document.body.scrollTop; + } + return { x: x, y: y }; + } + + // When the offset parent is the document body, we need to account for + // scroll offsets when we set `top` and `left`. (Unless the element is + // `position: fixed`; in that case we should always ignore scroll + // position.) + var pageXY = (isAbsolute && parent === document.body) ? pageScrollXY() : { x: 0, y: 0 }; + + // Set position. + if (options.setLeft) + styles.left = (p[0] + pageXY.x - delta[0] + options.offsetLeft) + 'px'; + if (options.setTop) + styles.top = (p[1] + pageXY.y - delta[1] + options.offsetTop) + 'px'; + + if (options.setWidth || options.setHeight) { + layout = Element.getLayout(source); + + // Use content box when setting width/height. If padding/border are + // different between source and target, that's for the user to fix; + // there's no good option for us. + if (options.setWidth) { + styles.width = layout.get('width') + 'px'; + } + if (options.setHeight) { + styles.height = layout.get('height') + 'px'; + } + } + + return Element.setStyle(element, styles); + } + + + Element.addMethods({ + getLayout: getLayout, + measure: measure, + getWidth: getWidth, + getHeight: getHeight, + getDimensions: getDimensions, + getOffsetParent: getOffsetParent, + cumulativeOffset: cumulativeOffset, + positionedOffset: positionedOffset, + cumulativeScrollOffset: cumulativeScrollOffset, + viewportOffset: viewportOffset, + absolutize: absolutize, + relativize: relativize, + scrollTo: scrollTo, + makePositioned: makePositioned, + undoPositioned: undoPositioned, + makeClipping: makeClipping, + undoClipping: undoClipping, + clonePosition: clonePosition + }); + + function isBody(element) { + return element.nodeName.toUpperCase() === 'BODY'; + } + + function isHtml(element) { + return element.nodeName.toUpperCase() === 'HTML'; + } + + function isDocument(element) { + return element.nodeType === Node.DOCUMENT_NODE; + } + + function isDetached(element) { + return element !== document.body && + !Element.descendantOf(element, document.body); + } + +})(); + +(function() { + /** + * document.viewport + * + * The `document.viewport` namespace contains methods that return information + * about the viewport — the rectangle that represents the portion of a web + * page within view. In other words, it's the browser window minus all chrome. + **/ + + var ROOT = null; + function getRootElement() { + if (ROOT) return ROOT; + ROOT = document.documentElement; + return ROOT; + } + + /** + * document.viewport.getDimensions() -> Object + * + * Returns an object containing viewport dimensions in the form + * `{ width: Number, height: Number }`. + * + * The _viewport_ is the subset of the browser window that a page occupies + * — the "usable" space in a browser window. + * + * ##### Example + * + * document.viewport.getDimensions(); + * //-> { width: 776, height: 580 } + **/ + function getDimensions() { + return { width: this.getWidth(), height: this.getHeight() }; + } + + /** + * document.viewport.getWidth() -> Number + * + * Returns the width of the viewport. + * + * Equivalent to calling `document.viewport.getDimensions().width`. + **/ + function getWidth() { + return getRootElement().clientWidth; + } + + /** + * document.viewport.getHeight() -> Number + * + * Returns the height of the viewport. + * + * Equivalent to `document.viewport.getDimensions().height`. + **/ + function getHeight() { + return getRootElement().clientHeight; + } + + /** + * document.viewport.getScrollOffsets() -> Array + * + * Returns the viewport's horizontal and vertical scroll offsets. + * + * Returns an array in the form of `[leftValue, topValue]`. Also accessible + * as properties: `{ left: leftValue, top: topValue }`. + * + * ##### Examples + * + * document.viewport.getScrollOffsets(); + * //-> { left: 0, top: 0 } + * + * window.scrollTo(0, 120); + * document.viewport.getScrollOffsets(); + * //-> { left: 0, top: 120 } + **/ + function getScrollOffsets() { + var x = window.pageXOffset || document.documentElement.scrollLeft || + document.body.scrollLeft; + var y = window.pageYOffset || document.documentElement.scrollTop || + document.body.scrollTop; + + return new Element.Offset(x, y); + } + + document.viewport = { + getDimensions: getDimensions, + getWidth: getWidth, + getHeight: getHeight, + getScrollOffsets: getScrollOffsets + }; + +})(); diff --git a/src/prototype/dom/selector.js b/src/prototype/dom/selector.js new file mode 100644 index 000000000..356e4ef01 --- /dev/null +++ b/src/prototype/dom/selector.js @@ -0,0 +1,169 @@ +/** section: DOM, related to: Prototype.Selector + * $$(cssRule...) -> [Element...] + * + * Takes an arbitrary number of CSS selectors (strings) and returns a document-order + * array of extended DOM elements that match any of them. + * + * Sometimes the usual tools from your DOM arsenal -- `document.getElementById` encapsulated + * by [[$]], `getElementsByTagName` and even Prototype's very own `getElementsByClassName` + * extensions -- just aren't enough to quickly find elements or collections of elements. + * If you know the DOM tree structure, you can simply resort to CSS selectors to get + * the job done. + * + * ##### Quick examples + * + * $$('div'); + * // -> all DIVs in the document. Same as document.getElementsByTagName('div'). + * // Nice addition, the elements you're getting back are already extended! + * + * $$('#contents'); + * // -> same as $('contents'), only it returns an array anyway (even though IDs must + * // be unique within a document). + * + * $$('li.faux'); + * // -> all LI elements with class 'faux' + * + * The [[$$]] function searches the entire document. For selector queries on more specific + * sections of a document, use [[Element.select]]. + * + * ##### Supported CSS syntax + * + * The [[$$]] function does not rely on the browser's internal CSS parsing capabilities + * (otherwise, we'd be in cross-browser trouble...), and therefore offers a consistent + * set of selectors across all supported browsers. + * + * ###### Supported in v1.5.0 + * + * * Type selector: tag names, as in `div`. + * * Descendant selector: the space(s) between other selectors, as in `#a li`. + * * Attribute selectors: the full CSS 2.1 set of `[attr]`, `[attr=value]`, `[attr~=value]` + * and `[attr|=value]`. It also supports `[attr!=value]`. If the value you're matching + * against includes a space, be sure to enclose the value in quotation marks (`[title="Hello World!"]`). + * * Class selector: CSS class names, as in `.highlighted` or `.example.wrong`. + * * ID selector: as in `#item1`. + * + * ###### Supported from v1.5.1 + * + * Virtually all of [CSS3](http://www.w3.org/TR/2001/CR-css3-selectors-20011113/#selectors) + * is supported, with the exception of pseudo-elements (like `::first-letter`) and some + * pseudo-classes (like `:hover`). Some examples of new selectors that can be used in 1.5.1: + * + * * Child selector: selects immediate descendants, as in `#a > li`. + * * Attribute selectors: all attribute operators are supported, including `~=` (matches + * part of a space-delimited attribute value, like `rel` or `class`); `^=` (matches the + * beginning of a value); `$=` (matches the end of a value); and `*=` (matches any part + * of the value). + * * The `:not` pseudo-class, as in `#a *:not(li)` (matches all descendants of `#a` that + * aren't LIs). + * * All the `:nth`, `:first`, and `:last` pseudo-classes. Examples include `tr:nth-child(even)` + * (all even table rows), `li:first-child` (the first item in any list), or `p:nth-last-of-type(3)` + * (the third-to-last paragraph on the page). + * * The `:empty` pseudo-class (for selecting elements without children or text content). + * * The `:enabled`, `:disabled`, and `:checked` pseudo-classes (for use with form controls). + * + * ##### Examples + * + * $$('#contents a[rel]'); + * // -> all links inside the element of ID "contents" with a rel attribute + * + * $$('a[href="#"]'); + * // -> all links with a href attribute of value "#" (eyeew!) + * + * $$('#navbar a', '#sidebar a'); + * // -> all links within the elements of ID "navbar" or "sidebar" + * + * **With version 1.5.1 and above** you can do various types of advanced selectors: + * + * $$('a:not([rel~=nofollow])'); + * // -> all links, excluding those whose rel attribute contains the word "nofollow" + * + * $$('table tbody > tr:nth-child(even)'); + * // -> all even rows within all table bodies + * + * $$('div:empty'); + * // -> all DIVs without content (i.e., whitespace-only) +**/ +var $$ = function() { + var expression = $A(arguments).join(', '); + return Prototype.Selector.select(expression, document); +}; + +/** + * Prototype.Selector + * + * A namespace that acts as a wrapper around + * the choosen selector engine (Sizzle by default). + * +**/ +Prototype.Selector = (function() { + + var K = Prototype.K; + + /** + * Prototype.Selector.select(expression[, root = document]) -> [Element...] + * - expression (String): A CSS selector. + * - root (Element | document): A "scope" to search within. All results will + * be descendants of this node. + * + * Searches `root` for elements that match the provided CSS selector and returns an + * array of extended [[Element]] objects. + **/ + function select() { + throw new Error('Method "Prototype.Selector.select" must be defined.'); + } + + /** + * Prototype.Selector.match(element, expression) -> Boolean + * - element (Element): a DOM element. + * - expression (String): A CSS selector. + * + * Tests whether `element` matches the CSS selector. + **/ + function match() { + throw new Error('Method "Prototype.Selector.match" must be defined.'); + } + + /** + * Prototype.Selector.find(elements, expression[, index = 0]) -> Element + * - elements (Enumerable): a collection of DOM elements. + * - expression (String): A CSS selector. + * - index (Number): Numeric index of the match to return, defaults to 0. + * + * Filters the given collection of elements with `expression` and returns the + * first matching element (or the `index`th matching element if `index` is + * specified). + **/ + function find(elements, expression, index) { + index = index || 0; + var match = Prototype.Selector.match, length = elements.length, matchIndex = 0, i; + + for (i = 0; i < length; i++) { + if (match(elements[i], expression) && index == matchIndex++) { + return elements[i]; + } + } + } + + /** alias of: Element.extend + * Prototype.Selector.extendElement(element) -> Element + **/ + var extendElement = K; + + /** + * Prototype.Selector.extendElements(elements) -> Enumerable + * - elements (Enumerable): a collection of DOM elements. + * + * If necessary, extends the elements contained in `elements` + * and returns `elements` untouched. This is provided as a + * convenience method for selector engine wrapper implementors. + **/ + var extendElements = K; + + return { + select: select, + match: match, + find: find, + extendElements: K, + extendElement: K + }; +})(); diff --git a/src/prototype/lang.js b/src/prototype/lang.js new file mode 100644 index 000000000..b24801cee --- /dev/null +++ b/src/prototype/lang.js @@ -0,0 +1,81 @@ +//= compat +//= require "lang/class" +//= require "lang/object" +//= require "lang/function" +//= require "lang/regexp" +//= require "lang/periodical_executer" +//= require "lang/string" +//= require "lang/template" +//= require "lang/enumerable" +//= require "lang/array" +//= require "lang/hash" +//= require "lang/number" +//= require "lang/range" + +/** + * == Language == + * + * Additions to JavaScript's "standard library" and extensions to + * built-in JavaScript objects. +**/ + +var Abstract = { }; + +/** section: Language + * Try +**/ + +/** deprecated + * Try.these(function...) -> ? + * - function (Function): A function that may throw an exception. + * + * Accepts an arbitrary number of functions and returns the result of the + * first one that doesn't throw an error. + * + * **This method is deprecated.** + * + *
More information
+ * + * [[Try.these]] provides a simple idiom for trying out blocks of code in + * sequence. Such a sequence of attempts usually represents a downgrading + * approach to obtaining a given feature. + * + * In this example from Prototype's [[Ajax section]] internals, we want to get an + * `XMLHttpRequest` object. Internet Explorer 8 and earlier, however, does not + * provide it as a vanilla JavaScript object, and will throw an error if we + * attempt a simple instantiation. Also, over time, its proprietary way + * evolved, changing COM interface names. + * + * [[Try.these]] will try several ways in sequence, from the best (and, + * theoretically, most widespread) one to the oldest and rarest way, returning + * the result of the first successful function. + * + * If none of the blocks succeeded, [[Try.these]] will return `undefined`, which + * will cause the `Ajax.getTransport` method in the example below to return + * `false`, provided as a fallback result value. + * + * var Ajax = { + * getTransport: function() { + * return Try.these( + * function() { return new XMLHttpRequest() }, + * function() { return new ActiveXObject('Msxml2.XMLHTTP') }, + * function() { return new ActiveXObject('Microsoft.XMLHTTP') } + * ) || false; + * } + * }; + **/ +var Try = { + these: function() { + var returnValue; + + for (var i = 0, length = arguments.length; i < length; i++) { + var lambda = arguments[i]; + try { + returnValue = lambda(); + break; + } catch (e) { } + } + + return returnValue; + } +}; diff --git a/src/prototype/lang/array.js b/src/prototype/lang/array.js new file mode 100644 index 000000000..f8d727d1b --- /dev/null +++ b/src/prototype/lang/array.js @@ -0,0 +1,483 @@ +/** section: Language, related to: 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]]. + * + * 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 []; + 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 + * + * 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: + * + * $w('apples bananas kiwis').each(function(fruit){ + * var message = 'I like ' + fruit + * // do something with the message + * }) + * + * 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(); + return string ? string.split(/\s+/) : []; +} + +/** alias of: $A + * Array.from(iterable) -> Array +**/ +Array.from = $A; + +/** section: Language + * class Array + * includes Enumerable + * + * Prototype extends all native JavaScript arrays with quite a few powerful + * methods. + * + * This is done in two ways: + * + * * It mixes in the [[Enumerable]] module, which brings in a ton of methods. + * * It adds quite a few extra methods, which are documented in this section. + * + * With Prototype, arrays become much, much more than the trivial objects we + * used to manipulate, limiting ourselves to using their `length` property and + * 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 + * + * 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 + * with Prototype. + * + * 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`. + * + * 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 + * of the `in` keyword. Only properties specifically marked as _non-enumerable_ + * are ignored by such a loop. By default, the `prototype` and `length` + * properties are so marked, which prevents you from enumerating over array + * methods when using for...in. This comfort led developers to use `for...in` as a + * shortcut for indexing loops, when it is not its actual purpose. + * + * However, Prototype has no way to mark the methods it adds to + * `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). + * + * ##### What you should use instead + * + * You can revert to vanilla loops: + * + * for (var index = 0; index < myArray.length; ++index) { + * var item = myArray[index]; + * // Your code working on item here... + * } + * + * Or you can use iterators, such as [[Array#each]]: + * + * myArray.each(function(item) { + * // 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 + * + * 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 + * every loop iteration) in methods like [[Array#each]] — _or_ relying on + * repetitive array construction (such as uniq), may yield unsatisfactory + * performance. In such cases, you're better off writing manual indexing loops, + * but take care then to cache the length property and use the prefix `++` + * operator: + * + * // Custom loop with cached length property: maximum full-loop + * // performance on very large arrays! + * for (var index = 0, len = myArray.length; index < len; ++index) { + * var item = myArray[index]; + * // Your code working on item here... + * } + * +**/ + +(function() { + var arrayProto = Array.prototype, + slice = arrayProto.slice, + _each = arrayProto.forEach; // use native browser JS 1.6 implementation if available + + // 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 + * + * var guys = ['Sam', 'Justin', 'Andrew', 'Dan']; + * guys.clear(); + * // -> [] + * guys + * // -> [] + **/ + function clear() { + this.length = 0; + return this; + } + + /** + * Array#first() -> ? + * + * Returns the array's first item (e.g., `array[0]`). + **/ + function first() { + return this[0]; + } + + /** + * Array#last() -> ? + * + * Returns the array's last item (e.g., `array[array.length - 1]`). + **/ + function last() { + return this[this.length - 1]; + } + + /** + * Array#compact() -> Array + * + * Returns a **copy** of the array without any `null` or `undefined` values. + * + * ##### Example + * + * var orig = [undefined, 'A', undefined, 'B', null, 'C']; + * var copy = orig.compact(); + * // orig -> [undefined, 'A', undefined, 'B', null, 'C']; + * // copy -> ['A', 'B', 'C']; + **/ + function compact() { + return this.select(function(value) { + return value != null; + }); + } + + /** + * Array#flatten() -> Array + * + * Returns a flattened (one-dimensional) copy of the array, leaving + * the original array unchanged. + * + * Nested arrays are recursively injected inline. This can prove very + * useful when handling the results of a recursive collection algorithm, + * for instance. + * + * ##### Example + * + * var a = ['frank', ['bob', 'lisa'], ['jill', ['tom', 'sally']]]; + * var b = a.flatten(); + * // a -> ['frank', ['bob', 'lisa'], ['jill', ['tom', 'sally']]] + * // b -> ['frank', 'bob', 'lisa', 'jill', 'tom', 'sally'] + **/ + function flatten() { + return this.inject([], function(array, value) { + if (Object.isArray(value)) + return array.concat(value.flatten()); + array.push(value); + return array; + }); + } + + /** + * Array#without(value[, value...]) -> Array + * - value (?): A value to exclude. + * + * Produces a new version of the array that does not contain any of the + * specified values, leaving the original array unchanged. + * + * ##### Examples + * + * [3, 5, 6].without(3) + * // -> [5, 6] + * + * [3, 5, 6, 20].without(20, 6) + * // -> [3, 5] + **/ + function without() { + var values = slice.call(arguments, 0); + return this.select(function(value) { + return !values.include(value); + }); + } + + /** + * Array#reverse([inline = true]) -> Array + * - inline (Boolean): Whether to modify the array in place. Defaults to `true`. + * Clones the original array when `false`. + * + * Reverses the array's contents, optionally cloning it first. + * + * ##### Examples + * + * // Making a copy + * var nums = [3, 5, 6, 1, 20]; + * var rev = nums.reverse(false); + * // nums -> [3, 5, 6, 1, 20] + * // rev -> [20, 1, 6, 5, 3] + * + * // Working inline + * var nums = [3, 5, 6, 1, 20]; + * nums.reverse(); + * // nums -> [20, 1, 6, 5, 3] + **/ + function reverse(inline) { + return (inline === false ? this.toArray() : this)._reverse(); + } + + /** + * Array#uniq([sorted = false]) -> Array + * - sorted (Boolean): Whether the array has already been sorted. If `true`, + * 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. + * + * On large arrays when `sorted` is `false`, this method has a potentially + * large performance cost. + * + * ##### Examples + * + * [1, 3, 2, 1].uniq(); + * // -> [1, 2, 3] + * + * ['A', 'a'].uniq(); + * // -> ['A', 'a'] (because String comparison is case-sensitive) + **/ + function uniq(sorted) { + return this.inject([], function(array, value, index) { + if (0 == index || (sorted ? array.last() != value : !array.include(value))) + array.push(value); + return array; + }); + } + + /** + * Array#intersect(array) -> Array + * - array (Array): A collection of values. + * + * Returns an array containing every item that is shared between the two + * given arrays. + **/ + function intersect(array) { + return this.uniq().findAll(function(item) { + return array.indexOf(item) !== -1; + }); + } + + /** alias of: Array#clone + * Array#toArray() -> Array + **/ + + /** + * Array#clone() -> Array + * + * Returns a duplicate of the array, leaving the original array intact. + **/ + function clone() { + return slice.call(this, 0); + } + + /** related to: Enumerable#size + * Array#size() -> Number + * + * Returns the size of the array (e.g., `array.length`). + * + * This is just a local optimization of the mixed-in [[Enumerable#size]] + * which avoids array cloning and uses the array's native length property. + **/ + function size() { + return this.length; + } + + /** related to: Object.inspect + * Array#inspect() -> String + * + * Returns the debug-oriented string representation of an array. + * + * ##### Example + * + * ['Apples', {good: 'yes', bad: 'no'}, 3, 34].inspect() + * // -> "['Apples', [object Object], 3, 34]" + **/ + function inspect() { + return '[' + this.map(Object.inspect).join(', ') + ']'; + } + + // 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. + + 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 { + // Pass straight through to the native method. + return method.apply(this, arguments); + } + }; + } + + // 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, + compact: compact, + flatten: flatten, + without: without, + reverse: reverse, + uniq: uniq, + intersect: intersect, + clone: clone, + toArray: clone, + size: size, + inspect: inspect + }); +})(); diff --git a/src/prototype/lang/class.js b/src/prototype/lang/class.js new file mode 100644 index 000000000..e0167f74f --- /dev/null +++ b/src/prototype/lang/class.js @@ -0,0 +1,179 @@ +/* Based on Alex Arnell's inheritance implementation. */ + +/** section: Language + * class Class + * + * Manages Prototype's class-based OOP system. + * + * Refer to Prototype's web site for a [tutorial on classes and + * inheritance](http://prototypejs.org/learn/class-inheritance). +**/ +var Class = (function() { + + /** + * Class.create([superclass][, methods...]) -> Class + * - superclass (Class): The optional superclass to inherit methods from. + * - methods (Object): An object whose properties will be "mixed-in" to the + * new class. Any number of mixins can be added; later mixins take + * precedence. + * + * [[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 + * 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 + * precedence over earlier arguments. + * + * If a subclass overrides an instance method declared in a superclass, the + * subclass's method can still access the original method. To do so, declare + * the subclass's method as normal, but insert `$super` as the first + * argument. This makes `$super` available as a method for use within the + * function. + * + * To extend a class after it has been defined, use [[Class#addMethods]]. + * + * For details, see the + * [inheritance tutorial](http://prototypejs.org/learn/class-inheritance) + * on the Prototype website. + **/ + function subclass() {}; + function create() { + var parent = null, properties = $A(arguments); + if (Object.isFunction(properties[0])) + parent = properties.shift(); + + function klass() { + this.initialize.apply(this, arguments); + } + + Object.extend(klass, Class.Methods); + klass.superclass = parent; + klass.subclasses = []; + + if (parent) { + subclass.prototype = parent.prototype; + klass.prototype = new subclass; + parent.subclasses.push(klass); + } + + for (var i = 0, length = properties.length; i < length; i++) + klass.addMethods(properties[i]); + + if (!klass.prototype.initialize) + klass.prototype.initialize = Prototype.emptyFunction; + + klass.prototype.constructor = klass; + return klass; + } + + /** + * Class#addMethods(methods) -> Class + * - methods (Object): The methods to add to the class. + * + * 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 + * to that class, or overwrite existing methods, after the class has been + * defined. + * + * New methods propagate down the inheritance chain. If the class has + * subclasses, those subclasses will receive the new methods — even in + * the context of `$super` calls. The new methods also propagate to instances + * of the class and of all its subclasses, even those that have already been + * instantiated. + * + * ##### Examples + * + * var Animal = Class.create({ + * initialize: function(name, sound) { + * this.name = name; + * this.sound = sound; + * }, + * + * speak: function() { + * alert(this.name + " says: " + this.sound + "!"); + * } + * }); + * + * // subclassing Animal + * var Snake = Class.create(Animal, { + * initialize: function($super, name) { + * $super(name, 'hissssssssss'); + * } + * }); + * + * var ringneck = new Snake("Ringneck"); + * ringneck.speak(); + * + * //-> alerts "Ringneck says: hissssssss!" + * + * // adding Snake#speak (with a supercall) + * Snake.addMethods({ + * speak: function($super) { + * $super(); + * alert("You should probably run. He looks really mad."); + * } + * }); + * + * ringneck.speak(); + * //-> alerts "Ringneck says: hissssssss!" + * //-> alerts "You should probably run. He looks really mad." + * + * // redefining Animal#speak + * Animal.addMethods({ + * speak: function() { + * alert(this.name + 'snarls: ' + this.sound + '!'); + * } + * }); + * + * ringneck.speak(); + * //-> alerts "Ringneck snarls: hissssssss!" + * //-> alerts "You should probably run. He looks really mad." + **/ + function addMethods(source) { + 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()[0] == "$super") { + var method = value; + value = (function(m) { + return function() { return ancestor[m].apply(this, arguments); }; + })(property).wrap(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; + } + + return this; + } + + return { + create: create, + Methods: { + addMethods: addMethods + } + }; +})(); diff --git a/src/prototype/lang/enumerable.js b/src/prototype/lang/enumerable.js new file mode 100644 index 000000000..f55eac347 --- /dev/null +++ b/src/prototype/lang/enumerable.js @@ -0,0 +1,853 @@ +/** section: Language + * mixin Enumerable + * + * [[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 standalone + * use, but for incorporation into other objects. + * + * 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 + * + * 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. + * + * var myObject = {}; + * + * ['foo', 'bar', 'baz'].each(function(name, index) { + * this[name] = index; + * }, myObject); // we have specified the context + * + * myObject; + * // -> { 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. + * + * ##### 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? + * + * 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]] + * 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. + * + * If you're still confused, just have a look at the Prototype source code for + * [[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 + * you'll usually do before defining your methods, so as to make sure whatever + * overrides you provide for [[Enumerable]] methods will indeed prevail. In + * short, your code will probably end up looking like this: + * + * + * var YourObject = Class.create(Enumerable, { + * initialize: function() { // with whatever constructor arguments you need + * // Your construction code + * }, + * + * _each: function(iterator) { + * // Your iteration code, invoking iterator at every turn + * }, + * + * // Your other methods here, including Enumerable overrides + * }); + * + * Then, obviously, your object can be used like this: + * + * var obj = new YourObject(); + * // Populate the collection somehow + * obj.pluck('somePropName'); + * obj.invoke('someMethodName'); + * obj.size(); + * // etc. + * +**/ + +var $break = { }; + +var Enumerable = (function() { + /** + * Enumerable#each(iterator[, context]) -> Enumerable + * - iterator (Function): A `Function` that expects an item in the + * collection as the first argument and a numerical index as the second. + * - context (Object): The scope in which to call `iterator`. Affects what + * the keyword `this` means inside `iterator`. + * + * Calls `iterator` for each item in the collection. + * + * ##### Examples + * + * ['one', 'two', 'three'].each(alert); + * // Alerts "one", then alerts "two", then alerts "three" + * + * ##### 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 + * matching item in an enumeration, or transform it, or determine whether it + * has any (or all) values matching a particular condition, [[Enumerable]] + * has a method to do that for you. + **/ + function each(iterator, context) { + try { + this._each(iterator, context); + } catch (e) { + if (e != $break) throw e; + } + return this; + } + + /** + * Enumerable#eachSlice(number[, iterator = Prototype.K[, context]]) -> Enumerable + * - number (Number): The number of items to include in each slice. + * - iterator (Function): An optional function to use to transform each + * element before it's included in the slice; if this is not provided, + * the element itself is included. + * - context (Object): An optional object to use as `this` within + * calls to the iterator. + * + * Groups items into chunks of the given size. The final "slice" may have + * fewer than `number` items; it won't "pad" the last group with empty + * values. For that behavior, use [[Enumerable#inGroupsOf]]. + * + * ##### Example + * + * var students = [ + * { name: 'Sunny', age: 20 }, + * { name: 'Audrey', age: 21 }, + * { name: 'Matt', age: 20 }, + * { name: 'Amelie', age: 26 }, + * { name: 'Will', age: 21 } + * ]; + * + * students.eachSlice(3, function(student) { + * return student.name; + * }); + * // -> [['Sunny', 'Audrey', 'Matt'], ['Amelie', 'Will']] + **/ + function eachSlice(number, iterator, context) { + var index = -number, slices = [], array = this.toArray(); + if (number < 1) return array; + while ((index += number) < array.length) + slices.push(array.slice(index, index+number)); + return slices.collect(iterator, context); + } + + /** + * Enumerable#all([iterator = Prototype.K[, context]]) -> Boolean + * - iterator (Function): An optional function to use to evaluate + * each element in the enumeration; the function should return the value to + * test. If this is not provided, the element itself is tested. + * - context (Object): An optional object to use as `this` within + * calls to the iterator. + * + * Determines whether all the elements are "truthy" (boolean-equivalent to + * `true`), either directly or through computation by the provided iterator. + * Stops on the first falsy element found (e.g., the first element that + * is boolean-equivalent to `false`, such as `undefined`, `0`, or indeed + * `false`); + * + * ##### Examples + * + * [].all(); + * // -> true (empty arrays have no elements that could be falsy) + * + * $R(1, 5).all(); + * // -> true (all values in [1..5] are truthy) + * + * [0, 1, 2].all(); + * // -> false (with only one loop cycle: 0 is falsy) + * + * [9, 10, 15].all(function(n) { return n >= 10; }); + * // -> false (the iterator returns false on 9) + **/ + function all(iterator, context) { + iterator = iterator || Prototype.K; + var result = true; + this.each(function(value, index) { + result = result && !!iterator.call(context, value, index, this); + if (!result) throw $break; + }, this); + return result; + } + + /** + * Enumerable#any([iterator = Prototype.K[, context]]) -> Boolean + * - iterator (Function): An optional function to use to evaluate each + * element in the enumeration; the function should return the value to + * test. If this is not provided, the element itself is tested. + * - context (Object): An optional object to use as `this` within + * calls to the iterator. + * + * Determines whether at least one element is truthy (boolean-equivalent to + * `true`), either directly or through computation by the provided iterator. + * + * ##### Examples + * + * [].any(); + * // -> false (empty arrays have no elements that could be truthy) + * + * $R(0, 2).any(); + * // -> true (on the second loop, 1 is truthy) + * + * [2, 4, 6, 8, 10].any(function(n) { return n > 5; }); + * // -> true (the iterator will return true on 6) + **/ + function any(iterator, context) { + iterator = iterator || Prototype.K; + var result = false; + this.each(function(value, index) { + if (result = !!iterator.call(context, value, index, this)) + throw $break; + }, this); + return result; + } + + /** + * Enumerable#collect([iterator = Prototype.K[, context]]) -> Array + * - iterator (Function): The iterator function to apply to each element + * in the enumeration. + * - context (Object): An optional object to use as `this` within + * calls to the iterator. + * + * Returns the result of applying `iterator` to each element. If no + * `iterator` is provided, the elements are simply copied to the + * returned array. + * + * ##### Examples + * + * ['Hitch', "Hiker's", 'Guide', 'to', 'the', 'Galaxy'].collect(function(s) { + * return s.charAt(0).toUpperCase(); + * }); + * // -> ['H', 'H', 'G', 'T', 'T', 'G'] + * + * $R(1,5).collect(function(n) { + * return n * n; + * }); + * // -> [1, 4, 9, 16, 25] + **/ + function collect(iterator, context) { + iterator = iterator || Prototype.K; + var results = []; + this.each(function(value, index) { + results.push(iterator.call(context, value, index, this)); + }, this); + return results; + } + + /** + * Enumerable#detect(iterator[, context]) -> firstElement | undefined + * - iterator (Function): The iterator function to apply to each element + * in the enumeration. + * - context (Object): An optional object to use as `this` within + * calls to the iterator. + * + * Returns the first element for which the iterator returns a truthy value. + * Aliased by the [[Enumerable#find]] method. + * + * ##### Example + * + * [1, 7, -2, -4, 5].detect(function(n) { return n < 0; }); + * // -> -2 + **/ + function detect(iterator, context) { + var result; + this.each(function(value, index) { + if (iterator.call(context, value, index, this)) { + result = value; + throw $break; + } + }, this); + return result; + } + + /** + * Enumerable#findAll(iterator[, context]) -> Array + * - iterator (Function): An iterator function to use to test the elements. + * - context (Object): An optional object to use as `this` within + * calls to the iterator. + * + * Returns all the elements for which the iterator returned a truthy value. + * For the opposite operation, see [[Enumerable#reject]]. + * + * ##### Example + * + * [1, 'two', 3, 'four', 5].findAll(Object.isString); + * // -> ['two', 'four'] + **/ + function findAll(iterator, context) { + var results = []; + this.each(function(value, index) { + if (iterator.call(context, value, index, this)) + results.push(value); + }, this); + return results; + } + + /** + * Enumerable#grep(filter[, iterator = Prototype.K[, context]]) -> Array + * - filter (RegExp | String | Object): The filter to apply to elements. This + * can be a `RegExp` instance, a regular expression [[String]], or any + * object with a `match` function. + * - iterator (Function): An optional function to apply to selected elements + * before including them in the result. + * - context (Object): An optional object to use as `this` within + * calls to the iterator. + * + * Returns an array containing all of the elements for which the given + * filter returns `true` (or a truthy value). If an iterator is provided, + * it is used to produce the returned value for each selected element; this + * is done *after* the element has been selected by the filter. + * + * If the given filter is a [[String]], it is converted into a `RegExp` + * object. To select elements, each element is passed into the filter's + * `match` function, which should return a truthy value to select the element + * or a falsy value not to. Note that the `RegExp` `match` function will + * convert elements to Strings to perform matching. + * + * ##### Examples + * + * // Get all strings containing a repeated letter + * ['hello', 'world', 'this', 'is', 'cool'].grep(/(.)\1/); + * // -> ['hello', 'cool'] + * + * // Get all numbers ending with 0 or 5 and subtract 1 from them + * $R(1, 30).grep(/[05]$/, function(n) { return n - 1; }); + * // -> [4, 9, 14, 19, 24, 29] + **/ + function grep(filter, iterator, context) { + iterator = iterator || Prototype.K; + var results = []; + + if (Object.isString(filter)) + filter = new RegExp(RegExp.escape(filter)); + + this.each(function(value, index) { + if (filter.match(value)) + results.push(iterator.call(context, value, index, this)); + }, this); + return results; + } + + /** + * Enumerable#include(object) -> Boolean + * - object (?): The object to look for. + * + * Determines whether a given object is in the enumerable or not, + * based on the `==` comparison operator (equality with implicit type + * conversion). + * + * ##### Examples + * + * $R(1, 15).include(10); + * // -> true + * + * ['hello', 'world'].include('HELLO'); + * // -> false ('hello' != 'HELLO') + * + * [1, 2, '3', '4', '5'].include(3); + * // -> true ('3' == 3) + **/ + function include(object) { + if (Object.isFunction(this.indexOf) && this.indexOf(object) != -1) + return true; + + var found = false; + this.each(function(value) { + if (value == object) { + found = true; + throw $break; + } + }); + return found; + } + + /** + * Enumerable#inGroupsOf(number[, fillWith = null]) -> [group...] + * - number (Number): The number of items to include in each group. + * - fillWith (Object): An optional filler to use if the last group needs + * any; defaults to `null`. + * + * Like [[Enumerable#eachSlice]], but pads out the last chunk with the + * specified value if necessary and doesn't support the `iterator` function. + * + * ##### Examples + * + * var students = [ + * { name: 'Sunny', age: 20 }, + * { name: 'Audrey', age: 21 }, + * { name: 'Matt', age: 20 }, + * { name: 'Amelie', age: 26 }, + * { name: 'Will', age: 21 } + * ]; + * + * students.inGroupsOf(2, { name: '', age: 0 }); + * // -> [ + * // [{ name: 'Sunny', age: 20 }, { name: 'Audrey', age: 21 }], + * // [{ name: 'Matt', age: 20 }, { name: 'Amelie', age: 26 }], + * // [{ name: 'Will', age: 21 }, { name: '', age: 0 }] + * // ] + **/ + function inGroupsOf(number, fillWith) { + fillWith = Object.isUndefined(fillWith) ? null : fillWith; + return this.eachSlice(number, function(slice) { + while(slice.length < number) slice.push(fillWith); + return slice; + }); + } + + /** + * Enumerable#inject(accumulator, iterator[, context]) -> accumulatedValue + * - accumulator (?): The initial value to which the `iterator` adds. + * - iterator (Function): An iterator function used to build the accumulated + * result. + * - context (Object): An optional object to use as `this` within + * calls to the iterator. + * + * Incrementally builds a result value based on the successive results + * of the iterator. This can be used for array construction, numerical + * sums/averages, etc. + * + * The `iterator` function is called once for each element in the + * enumeration, receiving the current value of the accumulator as its first + * argument, the element as its second argument, and the element's index as + * its third. It returns the new value for the accumulator. + * + * ##### Examples + * + * $R(1,10).inject(0, function(acc, n) { return acc + n; }); + * // -> 55 (sum of 1 to 10) + * + * ['a', 'b', 'c', 'd', 'e'].inject([], function(string, value, index) { + * if (index % 2 === 0) { // even numbers + * string += value; + * } + * return string; + * }); + * // -> 'ace' + **/ + function inject(memo, iterator, context) { + this.each(function(value, index) { + memo = iterator.call(context, memo, value, index, this); + }, this); + return memo; + } + + /** + * Enumerable#invoke(methodName[, arg...]) -> Array + * - methodName (String): The name of the method to invoke. + * - args (?): Optional arguments to pass to the method. + * + * 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 + * + * ['hello', 'world'].invoke('toUpperCase'); + * // -> ['HELLO', 'WORLD'] + * + * ['hello', 'world'].invoke('substring', 0, 3); + * // -> ['hel', 'wor'] + * + * $$('input').invoke('stopObserving', 'change'); + * // -> Stops observing the 'change' event on all input elements, + * // returns an array of the element references. + **/ + function invoke(method) { + var args = $A(arguments).slice(1); + return this.map(function(value) { + return value[method].apply(value, args); + }); + } + + /** related to: Enumerable#min + * Enumerable#max([iterator = Prototype.K[, context]]) -> maxValue + * - iterator (Function): An optional function to use to evaluate each + * element in the enumeration; the function should return the value to + * test. If this is not provided, the element itself is tested. + * - context (Object): An optional object to use as `this` within + * calls to the iterator. + * + * Returns the maximum element (or element-based `iterator` result), or + * `undefined` if the enumeration is empty. Elements are either compared + * directly, or by first calling `iterator` and comparing returned values. + * If multiple "max" elements (or results) are equivalent, the one closest + * to the end of the enumeration is returned. + * + * If provided, `iterator` is called with two arguments: The element being + * evaluated, and its index in the enumeration; it should return the value + * `max` should consider (and potentially return). + * + * ##### Examples + * + * ['c', 'b', 'a'].max(); + * // -> 'c' + * + * [1, 3, '3', 2].max(); + * // -> '3' (because both 3 and '3' are "max", and '3' was later) + * + * ['zero', 'one', 'two'].max(function(item) { return item.length; }); + * // -> 4 + **/ + function max(iterator, context) { + iterator = iterator || Prototype.K; + var result; + this.each(function(value, index) { + value = iterator.call(context, value, index, this); + if (result == null || value >= result) + result = value; + }, this); + return result; + } + + /** related to: Enumerable#max + * Enumerable#min([iterator = Prototype.K[, context]]) -> minValue + * - iterator (Function): An optional function to use to evaluate each + * element in the enumeration; the function should return the value to + * test. If this is not provided, the element itself is tested. + * - context (Object): An optional object to use as `this` within + * calls to the iterator. + * + * Returns the minimum element (or element-based `iterator` result), or + * `undefined` if the enumeration is empty. Elements are either compared + * directly, or by first calling `iterator` and comparing returned values. + * If multiple "min" elements (or results) are equivalent, the one closest + * to the *beginning* of the enumeration is returned. + * + * If provided, `iterator` is called with two arguments: The element being + * evaluated, and its index in the enumeration; it should return the value + * `min` should consider (and potentially return). + * + * ##### Examples + * + * ['c', 'b', 'a'].min(); + * // -> 'a' + * + * [3, 1, '1', 2].min(); + * // -> 1 (because both 1 and '1' are "min", and 1 was earlier) + * + * ['un', 'deux', 'trois'].min(function(item) { return item.length; }); + * // -> 2 + **/ + function min(iterator, context) { + iterator = iterator || Prototype.K; + var result; + this.each(function(value, index) { + value = iterator.call(context, value, index, this); + if (result == null || value < result) + result = value; + }, this); + return result; + } + + /** + * Enumerable#partition([iterator = Prototype.K[, context]]) -> [TrueArray, FalseArray] + * - iterator (Function): An optional function to use to evaluate each + * element in the enumeration; the function should return the value to + * test. If this is not provided, the element itself is tested. + * - context (Object): An optional object to use as `this` within + * calls to the iterator. + * + * Partitions the elements in two groups: those regarded as true, and those + * considered false. By default, regular JavaScript boolean equivalence + * (e.g., truthiness vs. falsiness) is used, but an iterator can be provided + * that computes a boolean representation of the elements. + * + * Using `partition` is more efficient than using [[Enumerable#findAll]] and + * then using [[Enumerable#reject]] because the enumeration is only processed + * once. + * + * ##### Examples + * + * ['hello', null, 42, false, true, , 17].partition(); + * // -> [['hello', 42, true, 17], [null, false, undefined]] + * + * $R(1, 10).partition(function(n) { + * return 0 == n % 2; + * }); + * // -> [[2, 4, 6, 8, 10], [1, 3, 5, 7, 9]] + **/ + function partition(iterator, context) { + iterator = iterator || Prototype.K; + var trues = [], falses = []; + this.each(function(value, index) { + (iterator.call(context, value, index, this) ? + trues : falses).push(value); + }, this); + return [trues, falses]; + } + + /** + * Enumerable#pluck(property) -> Array + * - property (String): The name of the property to fetch. + * + * Pre-baked implementation for a common use-case of [[Enumerable#collect]] + * and [[Enumerable#each]]: fetching the same property for all of the + * elements. Returns an array of the property values. + * + * ##### Example + * + * ['hello', 'world', 'this', 'is', 'nice'].pluck('length'); + * // -> [5, 5, 4, 2, 4] + **/ + function pluck(property) { + var results = []; + this.each(function(value) { + results.push(value[property]); + }); + return results; + } + + /** + * Enumerable#reject(iterator[, context]) -> Array + * - iterator (Function): An iterator function to use to test the elements. + * - context (Object): An optional object to use as `this` within + * calls to the iterator. + * + * Returns all the elements for which the iterator returns a falsy value. + * For the opposite operation, see [[Enumerable#findAll]]. + * + * ##### Example + * + * [1, "two", 3, "four", 5].reject(Object.isString); + * // -> [1, 3, 5] + **/ + function reject(iterator, context) { + var results = []; + this.each(function(value, index) { + if (!iterator.call(context, value, index, this)) + results.push(value); + }, this); + return results; + } + + /** + * Enumerable#sortBy(iterator[, context]) -> Array + * - iterator (Function): The function to use to compute the criterion for + * each element in the enumeration. + * - context (Object): An optional object to use as `this` within + * calls to the iterator. + * + * Creates a custom-sorted array of the elements based on the criteria + * computed, for each element, by the iterator. Computed criteria must have + * well-defined ordering semantics (i.e. the `<` operator must exist between + * any two criteria). + * + * [[Enumerable#sortBy]] does not guarantee a *stable* sort; adjacent + * equivalent elements may be swapped. + * + * ##### Example + * + * ['hello', 'world', 'this', 'is', 'nice'].sortBy(function(s) { + * return s.length; + * }); + * // -> ['is', 'nice', 'this', 'world', 'hello'] + **/ + function sortBy(iterator, context) { + return this.map(function(value, index) { + return { + value: value, + criteria: iterator.call(context, value, index, this) + }; + }, this).sort(function(left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }).pluck('value'); + } + + /** + * Enumerable#toArray() -> Array + * + * Returns an Array containing the elements of the enumeration. + * + * ##### Example + * + * $R(1, 5).toArray(); + * // -> [1, 2, 3, 4, 5] + * + * $H({ name: 'Sunny', age: 20 }).toArray(); + * // -> [['name', 'Sunny'], ['age', 20]] + **/ + function toArray() { + return this.map(); + } + + /** + * Enumerable#zip(sequence...[, iterator = Prototype.K]) -> Array + * - sequence (Object): A sequence to zip with this enumerable (there can + * be several of these if desired). + * - iterator (Function): Optional function to use to transform the tuples + * once generated; this is always the last argument provided. + * + * Zips together (think of the zipper on a pair of trousers) 2+ sequences, + * returning a new array of tuples. Each tuple is an array containing one + * value per original sequence. Tuples can be transformed to something else + * by applying the optional `iterator` on them. + * + * 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 + * + * var firstNames = ['Jane', 'Nitin', 'Guy']; + * var lastNames = ['Doe', 'Patel', 'Forcier']; + * var ages = [23, 41, 17]; + * + * firstNames.zip(lastNames); + * // -> [['Jane', 'Doe'], ['Nitin', 'Patel'], ['Guy', 'Forcier']] + * + * firstNames.zip(lastNames, ages); + * // -> [['Jane', 'Doe', 23], ['Nitin', 'Patel', 41], ['Guy', 'Forcier', 17]] + * + * firstNames.zip(lastNames, ages, function(tuple) { + * return tuple[0] + ' ' + tuple[1] + ' is ' + tuple[2]; + * }); + * // -> ['Jane Doe is 23', 'Nitin Patel is 41', 'Guy Forcier is 17'] + **/ + function zip() { + var iterator = Prototype.K, args = $A(arguments); + if (Object.isFunction(args.last())) + iterator = args.pop(); + + var collections = [this].concat(args).map($A); + return this.map(function(value, index) { + return iterator(collections.pluck(index)); + }); + } + + /** + * Enumerable#size() -> Number + * + * Returns the size of the enumeration. + **/ + function size() { + return this.toArray().length; + } + + /** + * Enumerable#inspect() -> String + * + * Returns the debug-oriented string representation of the object. + **/ + function inspect() { + return '#'; + } + + /** alias of: Enumerable#collect + * Enumerable#map([iterator = Prototype.K[, context]]) -> Array + **/ + + /** alias of: Enumerable#any + * Enumerable#some([iterator = Prototype.K[, context]]) -> Boolean + **/ + + /** alias of: Enumerable#all + * Enumerable#every([iterator = Prototype.K[, context]]) -> Boolean + **/ + + /** alias of: Enumerable#findAll + * Enumerable#select(iterator[, context]) -> Array + **/ + + /** alias of: Enumerable#findAll + * Enumerable#filter(iterator[, context]) -> Array + **/ + + /** alias of: Enumerable#include + * Enumerable#member(object) -> Boolean + **/ + + /** alias of: Enumerable#toArray + * Enumerable#entries() -> Array + **/ + + /** alias of: Enumerable#detect + * Enumerable#find(iterator[, context]) -> firstElement | undefined + **/ + + return { + each: each, + eachSlice: eachSlice, + all: all, + every: all, + any: any, + some: any, + collect: collect, + map: collect, + detect: detect, + findAll: findAll, + select: findAll, + filter: findAll, + grep: grep, + include: include, + member: include, + inGroupsOf: inGroupsOf, + inject: inject, + invoke: invoke, + max: max, + min: min, + partition: partition, + pluck: pluck, + reject: reject, + sortBy: sortBy, + toArray: toArray, + entries: toArray, + zip: zip, + size: size, + inspect: inspect, + find: detect + }; +})(); diff --git a/src/prototype/lang/function.js b/src/prototype/lang/function.js new file mode 100644 index 000000000..508e36fd6 --- /dev/null +++ b/src/prototype/lang/function.js @@ -0,0 +1,417 @@ +/** section: Language + * class Function + * + * Extensions to the built-in `Function` object. +**/ +Object.extend(Function.prototype, (function() { + var slice = Array.prototype.slice; + + function update(array, args) { + var arrayLength = array.length, length = args.length; + while (length--) array[arrayLength + length] = args[length]; + return array; + } + + function merge(array, args) { + array = slice.call(array, 0); + return update(array, args); + } + + /** + * Function#argumentNames() -> Array + * + * Reads the argument names as stated in the function definition and returns + * the values as an array of strings (or an empty array if the function is + * defined without parameters). + * + * ##### Examples + * + * function fn(foo, bar) { + * return foo + bar; + * } + * fn.argumentNames(); + * //-> ['foo', 'bar'] + * + * Prototype.emptyFunction.argumentNames(); + * //-> [] + **/ + function argumentNames() { + var names = this.toString().match(/^[\s\(]*function[^(]*\(([^)]*)\)/)[1] + .replace(/\/\/.*?[\r\n]|\/\*(?:.|[\r\n])*?\*\//g, '') + .replace(/\s+/g, '').split(','); + return names.length == 1 && !names[0] ? [] : names; + } + + /** related to: Function#bindAsEventListener + * Function#bind(context[, args...]) -> Function + * - context (Object): The object to bind to. + * - args (?): Optional additional arguments to curry for the function. + * + * Binds this function to the given `context` by wrapping it in another + * function and returning the wrapper. Whenever the resulting "bound" + * function is called, it will call the original ensuring that `this` is set + * to `context`. Also optionally curries arguments for the function. + * + * `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. + * + * ##### 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): + * + * var AlertOnClick = Class.create({ + * initialize: function(msg) { + * this.msg = msg; + * }, + * handleClick: function(event) { + * event.stop(); + * alert(this.msg); + * } + * }); + * var myalert = new AlertOnClick("Clicked!"); + * $('foo').observe('click', myalert.handleClick); // <= WRONG + * // -> If 'foo' is clicked, the alert will be blank; "this" is wrong + * $('bar').observe('click', myalert.handleClick.bind(myalert)); // <= RIGHT + * // -> If 'bar' is clicked, the alert will be "Clicked!" + * + * `bind` can also *curry* (burn in) arguments for the function if you + * provide them after the `context` argument: + * + * var Averager = Class.create({ + * initialize: function() { + * this.count = 0; + * this.total = 0; + * }, + * add: function(addend) { + * ++this.count; + * this.total += addend; + * }, + * getAverage: function() { + * return this.count == 0 ? NaN : this.total / this.count; + * } + * }); + * var a = new Averager(); + * var b = new Averager(); + * var aAdd5 = a.add.bind(a, 5); // Bind to a, curry 5 + * var aAdd10 = a.add.bind(a, 10); // Bind to a, curry 10 + * var bAdd20 = b.add.bind(b, 20); // Bind to b, curry 20 + * aAdd5(); + * aAdd10(); + * bAdd20(); + * bAdd20(); + * alert(a.getAverage()); + * // -> Alerts "7.5" (average of [5, 10]) + * alert(b.getAverage()); + * // -> Alerts "20" (average of [20, 20]) + * + * (To curry without binding, see [[Function#curry]].) + **/ + + function bind(context) { + 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); + + var bound = function() { + var a = merge(args, arguments); + // 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 + * Function#bindAsEventListener(context[, args...]) -> Function + * - context (Object): The object to bind to. + * - args (?): Optional arguments to curry after the event argument. + * + * An event-specific variant of [[Function#bind]] which ensures the function + * will recieve the current event object as the first argument when + * executing. + * + * It is not necessary to use `bindAsEventListener` for all bound event + * handlers; [[Function#bind]] works well for the vast majority of cases. + * `bindAsEventListener` is only needed when: + * + * - Using old-style DOM0 handlers rather than handlers hooked up via + * [[Event.observe]], because `bindAsEventListener` gets the event object + * from the right place (even on MSIE). (If you're using `Event.observe`, + * that's already handled.) + * - You want to bind an event handler and curry additional arguments but + * have those arguments appear after, rather than before, the event object. + * This mostly happens if the number of arguments will vary, and so you + * want to know the event object is the first argument. + * + * ##### Example + * + * var ContentUpdater = Class.create({ + * initialize: function(initialData) { + * this.data = Object.extend({}, initialData); + * }, + * // On an event, update the content in the elements whose + * // IDs are passed as arguments from our data + * updateTheseHandler: function(event) { + * var argIndex, id, element; + * event.stop(); + * for (argIndex = 1; argIndex < arguments.length; ++argIndex) { + * id = arguments[argIndex]; + * element = $(id); + * if (element) { + * element.update(String(this.data[id]).escapeHTML()); + * } + * } + * } + * }); + * var cu = new ContentUpdater({ + * dispName: 'Joe Bloggs', + * dispTitle: 'Manager ', + * dispAge: 47 + * }); + * // Using bindAsEventListener because of the variable arg lists: + * $('btnUpdateName').observe('click', + * cu.updateTheseHandler.bindAsEventListener(cu, 'dispName') + * ); + * $('btnUpdateAll').observe('click', + * cu.updateTheseHandler.bindAsEventListener(cu, 'dispName', 'dispTitle', 'dispAge') + * ); + **/ + function bindAsEventListener(context) { + var __method = this, args = slice.call(arguments, 1); + return function(event) { + var a = update([event || window.event], args); + return __method.apply(context, a); + } + } + + /** + * Function#curry(args...) -> Function + * - args (?): The arguments to curry. + * + * *Curries* (burns in) arguments to a function, returning a new function + * that when called with call the original passing in the curried arguments + * (along with any new ones): + * + * function showArguments() { + * alert($A(arguments).join(', ')); + * } + * showArguments(1, 2,, 3); + * // -> alerts "1, 2, 3" + * + * var f = showArguments.curry(1, 2, 3); + * f('a', 'b'); + * // -> alerts "1, 2, 3, a, b" + * + * [[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. + * + * The name "curry" comes from [mathematics](http://en.wikipedia.org/wiki/Currying). + **/ + function curry() { + if (!arguments.length) return this; + var __method = this, args = slice.call(arguments, 0); + return function() { + var a = merge(args, arguments); + return __method.apply(this, a); + } + } + + /** + * Function#delay(timeout[, args...]) -> Number + * - timeout (Number): The time, in seconds, to wait before calling the + * function. + * - args (?): Optional arguments to pass to the function when calling it. + * + * Schedules the function to run after the specified amount of time, passing + * any arguments given. + * + * Behaves much like `window.setTimeout`, but the timeout is in seconds + * rather than milliseconds. Returns an integer ID that can be used to + * clear the timeout with `window.clearTimeout` before it runs. + * + * To schedule a function to run as soon as the interpreter is idle, use + * [[Function#defer]]. + * + * ##### Example + * + * function showMsg(msg) { + * alert(msg); + * } + * showMsg.delay(0.1, "Hi there!"); + * // -> Waits a 10th of a second, then alerts "Hi there!" + **/ + function delay(timeout) { + var __method = this, args = slice.call(arguments, 1); + timeout = timeout * 1000; + return window.setTimeout(function() { + return __method.apply(__method, args); + }, timeout); + } + + /** + * Function#defer(args...) -> Number + * - args (?): Optional arguments to pass into the function. + * + * Schedules the function to run as soon as the interpreter is idle. + * + * A "deferred" function will not run immediately; rather, it will run as soon + * as the interpreter's call stack is empty. + * + * Behaves much like `window.setTimeout` with a delay set to `0`. Returns an + * ID that can be used to clear the timeout with `window.clearTimeout` before + * it runs. + * + * ##### Example + * + * function showMsg(msg) { + * alert(msg); + * } + * + * showMsg("One"); + * showMsg.defer("Two"); + * showMsg("Three"); + * // Alerts "One", then "Three", then (after a brief pause) "Two" + * // Note that "Three" happens before "Two" + **/ + function defer() { + var args = update([0.01], arguments); + return this.delay.apply(this, args); + } + + /** + * Function#wrap(wrapper) -> Function + * - wrapper (Function): The function to use as a wrapper. + * + * Returns a function "wrapped" around the original function. + * + * [[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 wrapper function is called with this signature: + * + * function wrapper(callOriginal[, args...]) + * + * ...where `callOriginal` is a function that can be used to call the + * original (wrapped) function (or not, as appropriate). (`callOriginal` is + * not a direct reference to the original function, there's a layer of + * indirection in-between that sets up the proper context \[`this` value\] for + * it.) + * + * ##### Example + * + * // Wrap String#capitalize so it accepts an additional argument + * String.prototype.capitalize = String.prototype.capitalize.wrap( + * function(callOriginal, eachWord) { + * if (eachWord && this.include(" ")) { + * // capitalize each word in the string + * return this.split(" ").invoke("capitalize").join(" "); + * } else { + * // proceed using the original function + * return callOriginal(); + * } + * }); + * + * "hello world".capitalize(); + * // -> "Hello world" (only the 'H' is capitalized) + * "hello world".capitalize(true); + * // -> "Hello World" (both 'H' and 'W' are capitalized) + **/ + function wrap(wrapper) { + var __method = this; + return function() { + var a = update([__method.bind(this)], arguments); + return wrapper.apply(this, a); + } + } + + /** + * Function#methodize() -> Function + * + * Wraps the function inside another function that, when called, pushes + * `this` to the original function as the first argument (with any further + * arguments following it). + * + * The `methodize` method transforms the original function that has an + * explicit first argument to a function that passes `this` (the current + * context) as an implicit first argument at call time. It is useful when we + * 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 + * + * // A function that sets a name on a target object + * function setName(target, name) { + * target.name = name; + * } + * + * // Use it + * obj = {}; + * setName(obj, 'Fred'); + * obj.name; + * // -> "Fred" + * + * // Make it a method of the object + * obj.setName = setName.methodize(); + * + * // Use the method instead + * obj.setName('Barney'); + * obj.name; + * // -> "Barney" + * + * The example above is quite simplistic. It's more useful to copy methodized + * functions to object prototypes so that new methods are immediately shared + * among instances. In the Prototype library, `methodize` is used in various + * places such as the DOM module, so that (for instance) you can hide an + * element either by calling the static version of `Element.hide` and passing in + * an element reference or ID, like so: + * + * Element.hide('myElement'); + * + * ...or if you already have an element reference, just calling the + * methodized form instead: + * + * myElement.hide(); + **/ + function methodize() { + if (this._methodized) return this._methodized; + var __method = this; + return this._methodized = function() { + var a = update([this], arguments); + return __method.apply(null, a); + }; + } + + var extensions = { + argumentNames: argumentNames, + 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/prototype/lang/hash.js b/src/prototype/lang/hash.js new file mode 100644 index 000000000..662640fcf --- /dev/null +++ b/src/prototype/lang/hash.js @@ -0,0 +1,408 @@ +/** section: Language, related to: Hash + * $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); +}; + +/** section: Language + * class Hash + * includes Enumerable + * + * A set of key/value pairs. + * + * [[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 + * methods that let you enumerate keys and values, iterate over key/value + * pairs, merge two hashes together, and much more. + * + * ##### 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]], + * it will be cloned. + * +**/ +var Hash = Class.create(Enumerable, (function() { + /** + * new Hash([object]) + * + * 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) { + this._object = Object.isHash(object) ? object.toObject() : Object.clone(object); + } + + // 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]] + * 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`. + * + * Iterates over the name/value pairs in the hash. + * + * This is actually just the [[Enumerable#each #each]] method from the + * mixed-in [[Enumerable]] module. It is documented here to describe the + * structure of the elements passed to the iterator and the order of + * iteration. + * + * The iterator's first argument (the "item") is an object with two + * properties: + * + * - `key`: the key name as a `String` + * - `value`: the corresponding value (which may be `undefined`) + * + * The order of iteration is implementation-dependent, as it relies on + * the order of the native `for..in` loop. Although most modern + * implementations exhibit *ordered* behavior, this is not standardized and + * may not always be the case, and so cannot be relied upon. + * + * ##### Example + * + * var h = $H({version: 1.6, author: 'The Core Team'}); + * + * h.each(function(pair) { + * alert(pair.key + ' = "' + pair.value + '"'); + * }); + * // Alerts 'version = "1.6"' and 'author = "The Core Team"' + * // -or- + * // Alerts 'author = "The Core Team"' and 'version = "1.6"' + **/ + + // Our _internal_ each + 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.call(context, pair, i); + i++; + } + } + + /** + * Hash#set(key, value) -> value + * - key (String): The key to use for this value. + * - value (?): The value to use for this key. + * + * Stores `value` in the hash using the key `key` and returns `value`. + * + * ##### Example + * + * var h = $H(); + * h.keys(); + * // -> [] (initially empty) + * h.set('a', 'apple'); + * // -> "apple" + * h.keys(); + * // -> ["a"] (has the new entry) + * h.get('a'); + * // -> "apple" + **/ + function set(key, value) { + return this._object[key] = value; + } + + /** + * Hash#get(key) -> value + * + * Returns the stored value for the given `key`. + * + * ##### Examples + * + * var h = new Hash({a: 'apple', b: 'banana', c: 'coconut'}); + * h.get('a'); + * // -> 'apple' + **/ + function get(key) { + // simulating poorly supported hasOwnProperty + if (this._object[key] !== Object.prototype[key]) + return this._object[key]; + } + + /** + * Hash#unset(key) -> value + * + * Deletes the stored pair for the given `key` from the hash and returns its + * value. + * + * ##### Example + * + * var h = new Hash({a: 'apple', b: 'banana', c: 'coconut'}); + * h.keys(); + * // -> ["a", "b", "c"] + * h.unset('a'); + * // -> 'apple' + * h.keys(); + * // -> ["b", "c"] ("a" is no longer in the hash) + **/ + function unset(key) { + var value = this._object[key]; + delete this._object[key]; + return value; + } + + /** + * Hash#toObject() -> Object + * + * Returns a cloned, vanilla object whose properties (and property values) + * match the keys (and values) from the hash. + * + * ##### Example + * + * var h = new Hash({ a: 'apple', b: 'banana', c: 'coconut' }); + * var obj = h.toObject(); + * obj.a; + * // -> "apple" + **/ + function toObject() { + 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...] + * + * Provides an Array containing the keys for items stored in the hash. + * + * The order of the keys is not guaranteed. + * + * ##### Example + * + * var h = $H({one: "uno", two: "due", three: "tre"}); + * h.keys(); + * // -> ["one", "three", "two"] (these may be in any order) + **/ + function keys() { + return this.pluck('key'); + } + + /** + * Hash#values() -> Array + * + * Collects the values of the hash and returns them in an array. + * + * The order of the values is not guaranteed. + * + * ##### Example + * + * var h = $H({one: "uno", two: "due", three: "tre"}); + * h.values(); + * // -> ["uno", "tre", "due"] (these may be in any order) + **/ + function values() { + return this.pluck('value'); + } + + /** + * Hash#index(value) -> String + * + * Returns the first key in the hash whose value matches `value`. + * Returns `false` if there is no such key. + **/ + function index(value) { + var match = this.detect(function(pair) { + return pair.value === value; + }); + return match && match.key; + } + + /** + * Hash#merge(object) -> Hash + * - 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; + * this hash remains unchanged. + * + * To modify the original hash in place, use [[Hash#update]]. + * + * ##### Example + * + * var h = $H({one: "uno", two: "due"}); + * var h2 = h.merge({three: "tre"}); + * h.keys(); + * // -> ["one", "two"] (unchanged) + * h2.keys(); + * // -> ["one", "two", "three"] (has merged contents) + **/ + function merge(object) { + return this.clone().update(object); + } + + /** + * Hash#update(object) -> Hash + * - object (Object | Hash): The object to merge with this hash to produce + * the resulting hash. + * + * Updates a hash *in place* with the key/value pairs of `object`, returns + * the hash. + * + * [[Hash#update]] modifies the hash. To get a new hash instead, use + * [[Hash#merge]]. + * + * ##### Example + * + * var h = $H({one: "uno", two: "due"}); + * h.update({three: "tre"}); + * // -> h (a reference to the original hash) + * h.keys(); + * // -> ["one", "two", "three"] (has merged contents) + **/ + function update(object) { + return new Hash(object).inject(this, function(result, pair) { + result.set(pair.key, pair.value); + return result; + }); + } + + // Private. No PDoc necessary. + function toQueryPair(key, value) { + if (Object.isUndefined(value)) return key; + + 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 + * Hash#toQueryString() -> String + * + * Returns a URL-encoded string containing the hash's contents as query + * parameters according to the following rules: + * + * - An undefined value results a parameter with no value portion at all + * (simply the key name, no equal sign). + * - A null value results a parameter with a blank value (the key followed + * by an equal sign and nothing else). + * - A boolean value results a parameter with the value "true" or "false". + * - An Array value results in a parameter for each array element, in + * array order, each using the same key. + * - All keys and values are URI-encoded using JavaScript's native + * `encodeURIComponent` function. + * + * The order of pairs in the string is not guaranteed, other than the order + * of array values described above. + * + * ##### Example + * + * $H({action: 'ship', + * order_id: 123, + * fees: ['f1', 'f2'] + * }).toQueryString(); + * // -> "action=ship&order_id=123&fees=f1&fees=f2" + * + * $H({comment: '', + * 'key with spaces': true, + * related_order: undefined, + * contents: null, + * 'label': 'a demo' + * }).toQueryString(); + * // -> "comment=&key%20with%20spaces=true&related_order&contents=&label=a%20demo" + * + * // an empty hash is an empty query string: + * $H().toQueryString(); + * // -> "" + **/ + 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)) { + // 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('&'); + } + + /** related to: Object.inspect + * Hash#inspect() -> String + * + * Returns the debug-oriented string representation of the Hash. + **/ + function inspect() { + return '#'; + } + + /** + * Hash#clone() -> Hash + * + * Returns a clone of this [[Hash]]. + **/ + function clone() { + return new Hash(this); + } + + return { + initialize: initialize, + _each: _each, + set: set, + get: get, + unset: unset, + toObject: toObject, + toTemplateReplacements: toObject, + keys: keys, + values: values, + index: index, + merge: merge, + update: update, + toQueryString: toQueryString, + inspect: inspect, + toJSON: toObject, + clone: clone + }; +})()); + +Hash.from = $H; diff --git a/src/prototype/lang/number.js b/src/prototype/lang/number.js new file mode 100644 index 000000000..f6acbc8de --- /dev/null +++ b/src/prototype/lang/number.js @@ -0,0 +1,158 @@ +/** section: Language + * class Number + * + * Extensions to the built-in `Number` object. + * + * Prototype extends native JavaScript numbers in order to provide: + * + * * [[ObjectRange]] compatibility, through [[Number#succ]]. + * * Numerical loops with [[Number#times]]. + * * Simple utility methods such as [[Number#toColorPart]] and + * [[Number#toPaddedString]]. + * * Instance-method aliases of many functions in the `Math` namespace. + * +**/ +Object.extend(Number.prototype, (function() { + /** + * Number#toColorPart() -> String + * + * Produces a 2-digit hexadecimal representation of the number + * (which is therefore assumed to be in the \[0..255\] range, inclusive). + * Useful for composing CSS color strings. + * + * ##### Example + * + * 10.toColorPart() + * // -> "0a" + **/ + function toColorPart() { + return this.toPaddedString(2, 16); + } + + /** + * Number#succ() -> Number + * + * Returns the successor of the current [[Number]], as defined by current + 1. + * Used to make numbers compatible with [[ObjectRange]]. + **/ + function succ() { + return this + 1; + } + + /** + * Number#times(iterator[,context]) -> Number + * - iterator (Function): An iterator function to call. + * - context (Object): An optional context (`this` value) to use when + * calling `iterator`. + * + * Calls `iterator` the specified number of times, passing in a number as + * 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 + * + * (3).times(alert); + * // -> Alerts "0", then "1", then "2"; returns 3 + * + * var obj = {count: 0, total: 0}; + * function add(addend) { + * ++this.count; + * this.total += addend; + * } + * (4).times(add, obj); + * // -> 4 + * obj.count; + * // -> 4 + * obj.total; + * // -> 6 (e.g., 0 + 1 + 2 + 3) + **/ + function times(iterator, context) { + $R(0, this, true).each(iterator, context); + return this; + } + + /** + * Number#toPaddedString(length[, radix]) -> String + * - length (Number): The minimum length for the resulting string. + * - radix (Number): An optional radix for the string representation, + * defaults to 10 (decimal). + * + * Returns a string representation of the number padded with leading 0s so + * 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 + * + * (13).toPaddedString(4); + * // -> "0013" + * + * (13).toPaddedString(2); + * // -> "13" + * + * (13).toPaddedString(1); + * // -> "13" + * + * (13).toPaddedString(4, 16) + * // -> "000d" + * + * (13).toPaddedString(4, 2); + * // -> "1101" + **/ + function toPaddedString(length, radix) { + var string = this.toString(radix || 10); + return '0'.times(length - string.length) + string; + } + + /** + * Number#abs() -> Number + * + * Returns the absolute value of the number. Convenience method that simply + * calls `Math.abs` on this instance and returns the result. + **/ + function abs() { + return Math.abs(this); + } + + /** + * Number#round() -> Number + * + * Rounds the number to the nearest integer. Convenience method that simply + * calls `Math.round` on this instance and returns the result. + **/ + function round() { + return Math.round(this); + } + + /** + * Number#ceil() -> Number + * + * Returns the smallest integer greater than or equal to the number. + * Convenience method that simply calls `Math.ceil` on this instance and + * returns the result. + **/ + function ceil() { + return Math.ceil(this); + } + + /** + * Number#floor() -> Number + * + * Returns the largest integer less than or equal to the number. + * Convenience method that simply calls `Math.floor` on this instance and + * returns the result. + **/ + function floor() { + return Math.floor(this); + } + + return { + toColorPart: toColorPart, + succ: succ, + times: times, + toPaddedString: toPaddedString, + abs: abs, + round: round, + ceil: ceil, + floor: floor + }; +})()); 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/prototype/lang/periodical_executer.js b/src/prototype/lang/periodical_executer.js new file mode 100644 index 000000000..519cabd43 --- /dev/null +++ b/src/prototype/lang/periodical_executer.js @@ -0,0 +1,85 @@ +/** section: Language + * class PeriodicalExecuter + * + * Oversees the calling of a particular function periodically. + * + * [[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 seconds, to wait in between + * callbacks. + * + * Creates a [[PeriodicalExecuter]]. + **/ + initialize: function(callback, frequency) { + this.callback = callback; + this.frequency = frequency; + this.currentlyExecuting = false; + + this.registerCallback(); + }, + + registerCallback: function() { + this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + execute: function() { + this.callback(this); + }, + + /** + * 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; + clearInterval(this.timer); + this.timer = null; + }, + + onTimerEvent: function() { + if (!this.currentlyExecuting) { + try { + this.currentlyExecuting = true; + this.execute(); + } finally { + this.currentlyExecuting = false; + } + } + } +}); 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 61% rename from src/lang/regexp.js rename to src/prototype/lang/regexp.js index d63b6c510..c1663408b 100644 --- a/src/lang/regexp.js +++ b/src/prototype/lang/regexp.js @@ -1,11 +1,15 @@ -/** section: lang - * RegExp +/** section: Language + * class RegExp + * + * Extensions to the built-in `RegExp` object. **/ -/** alias of: RegExp#test +/** * RegExp#match(str) -> Boolean - * - * Return true if string matches the regular expression, false otherwise. + * - str (String): a string against witch to match the regular expression. + * + * Alias of the native `RegExp#test` method. Returns `true` + * if `str` matches the regular expression, `false` otherwise. **/ RegExp.prototype.match = RegExp.prototype.test; diff --git a/src/prototype/lang/string.js b/src/prototype/lang/string.js new file mode 100644 index 000000000..433a1c0b1 --- /dev/null +++ b/src/prototype/lang/string.js @@ -0,0 +1,927 @@ +/** section: Language + * class String + * + * Extensions to the built-in `String` class. + * + * Prototype enhances the [[String]] object with a series of useful methods for + * ranging from the trivial to the complex. Tired of stripping trailing + * whitespace? Try [[String#strip]]. Want to replace `replace`? Have a look at + * [[String#sub]] and [[String#gsub]]. Need to parse a query string? We have + * [[String#toQueryParams what you need]]. +**/ +Object.extend(String, { + /** + * String.interpret(value) -> String + * + * Coerces `value` into a string. Returns an empty string for `null`. + **/ + interpret: function(value) { + return value == null ? '' : String(value); + }, + specialChar: { + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '\\': '\\\\' + } +}); + +Object.extend(String.prototype, (function() { + + function prepareReplacement(replacement) { + if (Object.isFunction(replacement)) return replacement; + var template = new Template(replacement); + return function(match) { return template.evaluate(match) }; + } + + // In some versions of Chrome, an empty RegExp has "(?:)" as a `source` + // property instead of an empty string. + function isNonEmptyRegExp(regexp) { + return regexp.source && regexp.source !== '(?:)'; + } + + + /** + * String#gsub(pattern, replacement) -> String + * + * Returns the string with _every_ occurence of a given pattern replaced by either a + * regular string, the returned value of a function or a [[Template]] string. + * The pattern can be a string or a regular expression. + * + * If its second argument is a string [[String#gsub]] works just like the native JavaScript + * method `replace()` set to global match. + * + * var mouseEvents = 'click dblclick mousedown mouseup mouseover mousemove mouseout'; + * + * mouseEvents.gsub(' ', ', '); + * // -> 'click, dblclick, mousedown, mouseup, mouseover, mousemove, mouseout' + * + * mouseEvents.gsub(/\s+/, ', '); + * // -> 'click, dblclick, mousedown, mouseup, mouseover, mousemove, mouseout' + * + * If you pass it a function, it will be invoked for every occurrence of the pattern + * with the match of the current pattern as its unique argument. Note that this argument + * is the returned value of the `match()` method called on the current pattern. It is + * in the form of an array where the first element is the entire match and every subsequent + * one corresponds to a parenthesis group in the regex. + * + * mouseEvents.gsub(/\w+/, function(match){ return 'on' + match[0].capitalize() }); + * // -> 'onClick onDblclick onMousedown onMouseup onMouseover onMousemove onMouseout' + * + * var markdown = '![a pear](/img/pear.jpg) ![an orange](/img/orange.jpg)'; + * + * markdown.gsub(/!\[(.*?)\]\((.*?)\)/, function(match) { + * return '' + match[1] + ''; + * }); + * // -> 'a pear an orange' + * + * Lastly, you can pass [[String#gsub]] a [[Template]] string in which you can also access + * the returned value of the `match()` method using the ruby inspired notation: `#{0}` + * for the first element of the array, `#{1}` for the second one, and so on. + * So our last example could be easily re-written as: + * + * markdown.gsub(/!\[(.*?)\]\((.*?)\)/, '#{1}'); + * // -> 'a pear an orange' + * + * If you need an equivalent to [[String#gsub]] but without global match set on, try [[String#sub]]. + * + * ##### Note + * + * Do _not_ use the `"g"` flag on the regex as this will create an infinite loop. + **/ + function gsub(pattern, replacement) { + var result = '', source = this, match; + replacement = prepareReplacement(replacement); + + if (Object.isString(pattern)) + pattern = RegExp.escape(pattern); + + if (!(pattern.length || isNonEmptyRegExp(pattern))) { + replacement = replacement(''); + return replacement + source.split('').join(replacement) + replacement; + } + + while (source.length > 0) { + match = source.match(pattern) + if (match && match[0].length > 0) { + result += source.slice(0, match.index); + result += String.interpret(replacement(match)); + source = source.slice(match.index + match[0].length); + } else { + result += source, source = ''; + } + } + return result; + } + + /** + * String#sub(pattern, replacement[, count = 1]) -> String + * + * Returns a string with the _first_ `count` occurrences of `pattern` replaced by either + * a regular string, the returned value of a function or a [[Template]] string. + * `pattern` can be a string or a regular expression. + * + * Unlike [[String#gsub]], [[String#sub]] takes a third optional parameter which specifies + * the number of occurrences of the pattern which will be replaced. + * If not specified, it will default to 1. + * + * Apart from that, [[String#sub]] works just like [[String#gsub]]. + * Please refer to it for a complete explanation. + * + * ##### Examples + * + * var fruits = 'apple pear orange'; + * + * fruits.sub(' ', ', '); + * // -> 'apple, pear orange' + * + * fruits.sub(' ', ', ', 1); + * // -> 'apple, pear orange' + * + * fruits.sub(' ', ', ', 2); + * // -> 'apple, pear, orange' + * + * fruits.sub(/\w+/, function(match){ return match[0].capitalize() + ',' }, 2); + * // -> 'Apple, Pear, orange' + * + * var markdown = '![a pear](/img/pear.jpg) ![an orange](/img/orange.jpg)'; + * + * markdown.sub(/!\[(.*?)\]\((.*?)\)/, function(match) { + * return '' + match[1] + ''; + * }); + * // -> 'a pear ![an orange](/img/orange.jpg)' + * + * markdown.sub(/!\[(.*?)\]\((.*?)\)/, '#{1}'); + * // -> 'a pear ![an orange](/img/orange.jpg)' + * + * ##### Note + * + * Do _not_ use the `"g"` flag on the regex as this will create an infinite loop. + **/ + function sub(pattern, replacement, count) { + replacement = prepareReplacement(replacement); + count = Object.isUndefined(count) ? 1 : count; + + return this.gsub(pattern, function(match) { + if (--count < 0) return match[0]; + return replacement(match); + }); + } + + /** related to: String#gsub + * String#scan(pattern, iterator) -> String + * + * Allows iterating over every occurrence of the given pattern (which can be a + * string or a regular expression). + * Returns the original string. + * + * Internally just calls [[String#gsub]] passing it `pattern` and `iterator` as arguments. + * + * ##### Examples + * + * 'apple, pear & orange'.scan(/\w+/, alert); + * // -> 'apple pear & orange' (and displays 'apple', 'pear' and 'orange' in three successive alert dialogs) + * + * Can be used to populate an array: + * + * var fruits = []; + * 'apple, pear & orange'.scan(/\w+/, function(match) { fruits.push(match[0]) }); + * fruits.inspect() + * // -> ['apple', 'pear', 'orange'] + * + * or even to work on the DOM: + * + * 'failure-message, success-message & spinner'.scan(/(\w|-)+/, Element.toggle) + * // -> 'failure-message, success-message & spinner' (and toggles the visibility of each DOM element) + * + * ##### Note + * + * Do _not_ use the `"g"` flag on the regex as this will create an infinite loop. + **/ + function scan(pattern, iterator) { + this.gsub(pattern, iterator); + return String(this); + } + + /** + * String#truncate([length = 30[, suffix = '...']]) -> String + * + * Truncates a string to given `length` and appends `suffix` to it (indicating + * that it is only an excerpt). + * + * ##### Examples + * + * 'A random sentence whose length exceeds 30 characters.'.truncate(); + * // -> 'A random sentence whose len...' + * + * 'Some random text'.truncate(); + * // -> 'Some random text.' + * + * 'Some random text'.truncate(10); + * // -> 'Some ra...' + * + * 'Some random text'.truncate(10, ' [...]'); + * // -> 'Some [...]' + **/ + function truncate(length, truncation) { + length = length || 30; + truncation = Object.isUndefined(truncation) ? '...' : truncation; + return this.length > length ? + this.slice(0, length - truncation.length) + truncation : String(this); + } + + /** + * String#strip() -> String + * + * Strips all leading and trailing whitespace from a string. + * + * ##### Example + * + * ' hello world! '.strip(); + * // -> 'hello world!' + **/ + function strip() { + return this.replace(/^\s+/, '').replace(/\s+$/, ''); + } + + /** + * String#stripTags() -> String + * + * Strips a string of any HTML tags. + * + * Note that [[String#stripTags]] will only strip HTML 4.01 tags — like + * `div`, `span`, and `abbr`. It _will not_ strip namespace-prefixed tags + * such as `h:table` or `xsl:template`. + * + * Watch out for `'.stripTags(); + * // -> 'a linkalert("hello world!");' + * + * 'a link'.stripScripts().stripTags(); + * // -> 'a link' + **/ + function stripTags() { + return this.replace(/<\w+(\s+("[^"]*"|'[^']*'|[^>])+)?(\/)?>|<\/\w+>/gi, ''); + } + + /** + * String#stripScripts() -> String + * + * Strips a string of things that look like HTML script blocks. + * + * ##### Example + * + * "

This is a test.End of test

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

This is a test.End of test

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

Scope test - scope of the handler should be this element

- +

Event object test - should be present as a first argument

- +

Hijack link test (preventDefault)

- + - +

Mouse click: left middle right

- + - +

Context menu event (tries to prevent default)

- +

Event.element() test

- +

Event.currentTarget test

- + - +

Event.findElement() test

- + - +

Stop propagation test (bubbling)

- + - +

Keyup test - focus on the textarea and type

- + - +

bindAsEventListener() test

- + - +

Object.inspect(event) test

- + - + +

mouseenter test

+ + +

Add unload events

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

before text

more testing

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

before text

more testing

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

after text

more testing

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

after text

more testing

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

top text.

more testing

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

top text.

more testing

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

bottom text.

more testing

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

bottom text.

more testing

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

node before

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

node after

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

node top

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

node bottom

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

toelement

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

bottom toelement

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

toHTML

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

tohtml

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

bottom toHTML

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

bottom tohtml

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

3

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

a paragraph

' }); - this.assertEqual('

a paragraph

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

a paragraph

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

a paragraph

' }); - this.assertEqual('

a paragraph

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

a paragraph

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

Hello world!

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

before text

more testing

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

before text

more testing

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

after text

more testing

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

after text

more testing

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

top text.

more testing

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

top text.

more testing

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

bottom text.

more testing

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

bottom text.

more testing

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

node before

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

node after

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

node top

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

node bottom

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

toelement

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

bottom toelement

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

toHTML

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

tohtml

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

bottom toHTML

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

bottom tohtml

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

3

'), '

3

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

a paragraph

' }); + assert.equal('

a paragraph

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

a paragraph

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

a paragraph

' }); + assert.equal('

a paragraph

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

a paragraph

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

world



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

Scroll test

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

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

-

- - +

+ +

content

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

+ + +
+ +
+

+ + + +
+ +
+

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

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

+
+ + + + + +
+

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

+
+ + + + + +
+

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

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

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

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

Some title here

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

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

Some title here

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

Some title here

- +
@@ -53,17 +53,36 @@

Some title here

+
+ + + + + + + + + +
-
grandfather -
father +
grandfather +
father
son
uncle
-
+
+ +
blah
+ +
+
+ +
+
diff --git a/test/unit/views/tests/selector_engine.erb b/test/unit/views/tests/selector_engine.erb new file mode 100644 index 000000000..4e8c2079b --- /dev/null +++ b/test/unit/views/tests/selector_engine.erb @@ -0,0 +1,4 @@ +
+
+
+
\ No newline at end of file diff --git a/src/dom/selector.js b/vendor/legacy_selector/repository/legacy_selector.js similarity index 70% rename from src/dom/selector.js rename to vendor/legacy_selector/repository/legacy_selector.js index 11486c75b..288b7571c 100644 --- a/src/dom/selector.js +++ b/vendor/legacy_selector/repository/legacy_selector.js @@ -1,70 +1,46 @@ -/* Portions of the Selector class are derived from Jack Slocum's DomQuery, +/* Portions of the Prototype.LegacySelector class are derived from Jack Slocum's DomQuery, * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style * license. Please see http://www.yui-ext.com/ for more information. */ -var Selector = Class.create({ +Prototype.LegacySelector = Class.create({ initialize: function(expression) { this.expression = expression.strip(); - + if (this.shouldUseSelectorsAPI()) { this.mode = 'selectorsAPI'; - } else if (this.shouldUseXPath()) { - this.mode = 'xpath'; - this.compileXPathMatcher(); } else { this.mode = "normal"; this.compileMatcher(); } - }, - - shouldUseXPath: function() { - if (!Prototype.BrowserFeatures.XPath) return false; - - var e = this.expression; - - // Safari 3 chokes on :*-of-type and :empty - if (Prototype.Browser.WebKit && - (e.include("-of-type") || e.include(":empty"))) - return false; - - // XPath can't do namespaced attributes, nor can it read - // the "checked" property from DOM nodes - if ((/(\[[\w-]*?:|:checked)/).test(e)) - return false; - return true; - }, - shouldUseSelectorsAPI: function() { if (!Prototype.BrowserFeatures.SelectorsAPI) return false; - - if (Selector.CASE_INSENSITIVE_CLASS_NAMES) return false; - - if (!Selector._div) Selector._div = new Element('div'); - // Make sure the browser treats the selector as valid. Test on an - // isolated element to minimize cost of this check. + if (!Prototype.LegacySelector._div) Prototype.LegacySelector._div = new Element('div'); + + // Make sure the browser treats the selector as valid. Test on an + // isolated element to minimize cost of this check. try { - Selector._div.querySelector(this.expression); + Prototype.LegacySelector._div.querySelector(this.expression); } catch(e) { return false; } - - return true; + + return true; }, - + compileMatcher: function() { - var e = this.expression, ps = Selector.patterns, h = Selector.handlers, - c = Selector.criteria, le, p, m, len = ps.length, name; + var e = this.expression, ps = Prototype.LegacySelector.patterns, h = Prototype.LegacySelector.handlers, + c = Prototype.LegacySelector.criteria, le, p, m, len = ps.length, name; - if (Selector._cache[e]) { - this.matcher = Selector._cache[e]; + if (Prototype.LegacySelector._cache[e]) { + this.matcher = Prototype.LegacySelector._cache[e]; return; } - - this.matcher = ["this.matcher = function(root) {", - "var r = root, h = Selector.handlers, c = false, n;"]; + + this.matcher = ["this.matcher = function(root) {", + "var r = root, h = Prototype.LegacySelector.handlers, c = false, n;"]; while (e && le != e && (/\S/).test(e)) { le = e; @@ -79,18 +55,18 @@ var Selector = Class.create({ } } } - + this.matcher.push("return h.unique(n);\n}"); eval(this.matcher.join('\n')); - Selector._cache[this.expression] = this.matcher; + Prototype.LegacySelector._cache[this.expression] = this.matcher; }, - + compileXPathMatcher: function() { - var e = this.expression, ps = Selector.patterns, - x = Selector.xpath, le, m, len = ps.length, name; + var e = this.expression, ps = Prototype.LegacySelector.patterns, + x = Prototype.LegacySelector.xpath, le, m, len = ps.length, name; - if (Selector._cache[e]) { - this.xpath = Selector._cache[e]; return; + if (Prototype.LegacySelector._cache[e]) { + this.xpath = Prototype.LegacySelector._cache[e]; return; } this.matcher = ['.//*']; @@ -99,22 +75,22 @@ var Selector = Class.create({ for (var i = 0; i"; + return "#"; } }); -if (Prototype.BrowserFeatures.SelectorsAPI && - document.compatMode === 'BackCompat') { - // Versions of Safari 3 before 3.1.2 treat class names case-insensitively in - // quirks mode. If we detect this behavior, we should use a different - // approach. - Selector.CASE_INSENSITIVE_CLASS_NAMES = (function(){ - var div = document.createElement('div'), - span = document.createElement('span'); - - div.id = "prototype_test_id"; - span.className = 'Test'; - div.appendChild(span); - var isIgnored = (div.querySelector('#prototype_test_id .test') !== null); - div = span = null; - return isIgnored; - })(); -} - -Object.extend(Selector, { +Prototype.LegacySelector.CASE_INSENSITIVE_CLASS_NAMES = false; + +Object.extend(Prototype.LegacySelector, { _cache: { }, - + xpath: { descendant: "//*", child: "/*", adjacent: "/following-sibling::*[1]", laterSibling: '/following-sibling::*', - tagName: function(m) { + tagName: function(m) { if (m[1] == '*') return ''; - return "[local-name()='" + m[1].toLowerCase() + + return "[local-name()='" + m[1].toLowerCase() + "' or local-name()='" + m[1].toUpperCase() + "']"; }, className: "[contains(concat(' ', @class, ' '), ' #{1} ')]", @@ -222,13 +184,13 @@ Object.extend(Selector, { attr: function(m) { m[1] = m[1].toLowerCase(); m[3] = m[5] || m[6]; - return new Template(Selector.xpath.operators[m[2]]).evaluate(m); + return new Template(Prototype.LegacySelector.xpath.operators[m[2]]).evaluate(m); }, pseudo: function(m) { - var h = Selector.xpath.pseudos[m[1]]; + var h = Prototype.LegacySelector.xpath.pseudos[m[1]]; if (!h) return ''; if (Object.isFunction(h)) return h(m); - return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m); + return new Template(Prototype.LegacySelector.xpath.pseudos[m[1]]).evaluate(m); }, operators: { '=': "[@#{1}='#{3}']", @@ -248,14 +210,14 @@ Object.extend(Selector, { 'disabled': "[(@disabled) and (@type!='hidden')]", 'enabled': "[not(@disabled) and (@type!='hidden')]", 'not': function(m) { - var e = m[6], p = Selector.patterns, - x = Selector.xpath, le, v, len = p.length, name; - + var e = m[6], p = Prototype.LegacySelector.patterns, + x = Prototype.LegacySelector.xpath, le, v, len = p.length, name; + var exclusion = []; while (e && le != e && (/\S/).test(e)) { le = e; for (var i = 0; i]))/ }, - { name: 'attrPresence', re: /^\[((?:[\w]+:)?[\w]+)\]/ }, + { name: 'attrPresence', re: /^\[((?:[\w-]+:)?[\w-]+)\]/ }, { name: 'attr', re: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/ } ], - - // for Selector.match and Element#match + + // for Prototype.LegacySelector.match and Element#match assertions: { tagName: function(element, matches) { return matches[1].toUpperCase() == element.tagName.toUpperCase(); @@ -362,10 +324,10 @@ Object.extend(Selector, { attr: function(element, matches) { var nodeValue = Element.readAttribute(element, matches[1]); - return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]); - } + return nodeValue && Prototype.LegacySelector.operators[matches[2]](nodeValue, matches[5] || matches[6]); + } }, - + handlers: { // UTILITY FUNCTIONS // joins two collections @@ -374,7 +336,7 @@ Object.extend(Selector, { a.push(node); return a; }, - + // marks an array of nodes for counting mark: function(nodes) { var _true = Prototype.emptyFunction; @@ -382,12 +344,34 @@ Object.extend(Selector, { node._countedByPrototype = _true; return nodes; }, - - unmark: function(nodes) { - for (var i = 0, node; node = nodes[i]; i++) - node._countedByPrototype = undefined; - return nodes; - }, + + unmark: (function(){ + + // IE improperly serializes _countedByPrototype in (inner|outer)HTML + // due to node properties being mapped directly to attributes + var PROPERTIES_ATTRIBUTES_MAP = (function(){ + var el = document.createElement('div'), + isBuggy = false, + propName = '_countedByPrototype', + value = 'x'; + el[propName] = value; + isBuggy = (el.getAttribute(propName) === value); + el = null; + return isBuggy; + })(); + + return PROPERTIES_ATTRIBUTES_MAP ? + function(nodes) { + for (var i = 0, node; node = nodes[i]; i++) + node.removeAttribute('_countedByPrototype'); + return nodes; + } : + function(nodes) { + for (var i = 0, node; node = nodes[i]; i++) + node._countedByPrototype = void 0; + return nodes; + } + })(), // mark each child node with its position (for nth calls) // "ofType" flag indicates whether we're indexing for nth-of-type @@ -404,36 +388,37 @@ Object.extend(Selector, { if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++; } }, - + // filters out duplicates and extends all nodes unique: function(nodes) { if (nodes.length == 0) return nodes; var results = [], n; for (var i = 0, l = nodes.length; i < l; i++) - if (!(n = nodes[i])._countedByPrototype) { + // use `typeof` operator to prevent errors + if (typeof (n = nodes[i])._countedByPrototype == 'undefined') { n._countedByPrototype = Prototype.emptyFunction; - results.push(Element.extend(n)); + results.push(n); } - return Selector.handlers.unmark(results); + return Prototype.LegacySelector.handlers.unmark(results); }, - + // COMBINATOR FUNCTIONS descendant: function(nodes) { - var h = Selector.handlers; + var h = Prototype.LegacySelector.handlers; for (var i = 0, results = [], node; node = nodes[i]; i++) h.concat(results, node.getElementsByTagName('*')); return results; }, - + child: function(nodes) { - var h = Selector.handlers; + var h = Prototype.LegacySelector.handlers; for (var i = 0, results = [], node; node = nodes[i]; i++) { for (var j = 0, child; child = node.childNodes[j]; j++) if (child.nodeType == 1 && child.tagName != '!') results.push(child); } return results; }, - + adjacent: function(nodes) { for (var i = 0, results = [], node; node = nodes[i]; i++) { var next = this.nextElementSibling(node); @@ -441,9 +426,9 @@ Object.extend(Selector, { } return results; }, - + laterSibling: function(nodes) { - var h = Selector.handlers; + var h = Prototype.LegacySelector.handlers; for (var i = 0, results = [], node; node = nodes[i]; i++) h.concat(results, Element.nextSiblings(node)); return results; @@ -454,17 +439,17 @@ Object.extend(Selector, { if (node.nodeType == 1) return node; return null; }, - + previousElementSibling: function(node) { while (node = node.previousSibling) if (node.nodeType == 1) return node; return null; }, - + // TOKEN FUNCTIONS tagName: function(nodes, root, tagName, combinator) { var uTagName = tagName.toUpperCase(); - var results = [], h = Selector.handlers; + var results = [], h = Prototype.LegacySelector.handlers; if (nodes) { if (combinator) { // fastlane for ordinary descendant combinators @@ -480,11 +465,26 @@ Object.extend(Selector, { return results; } else return root.getElementsByTagName(tagName); }, - + id: function(nodes, root, id, combinator) { - var targetNode = $(id), h = Selector.handlers; - if (!targetNode) return []; - if (!nodes && root == document) return [targetNode]; + var targetNode = $(id), h = Prototype.LegacySelector.handlers; + + if (root == document) { + // We don't have to deal with orphan nodes. Easy. + if (!targetNode) return []; + if (!nodes) return [targetNode]; + } else { + // In IE, we can check sourceIndex to see if root is attached + // to the document. If not (or if sourceIndex is not present), + // we do it the hard way. + if (!root.sourceIndex || root.sourceIndex < 1) { + var nodes = root.getElementsByTagName('*'); + for (var j = 0, node; node = nodes[j]; j++) { + if (node.id === id) return [node]; + } + } + } + if (nodes) { if (combinator) { if (combinator == 'child') { @@ -495,10 +495,10 @@ Object.extend(Selector, { if (Element.descendantOf(targetNode, node)) return [targetNode]; } else if (combinator == 'adjacent') { for (var i = 0, node; node = nodes[i]; i++) - if (Selector.handlers.previousElementSibling(targetNode) == node) + if (Prototype.LegacySelector.handlers.previousElementSibling(targetNode) == node) return [targetNode]; } else nodes = h[combinator](nodes); - } + } for (var i = 0, node; node = nodes[i]; i++) if (node == targetNode) return [targetNode]; return []; @@ -508,11 +508,11 @@ Object.extend(Selector, { className: function(nodes, root, className, combinator) { if (nodes && combinator) nodes = this[combinator](nodes); - return Selector.handlers.byClassName(nodes, root, className); + return Prototype.LegacySelector.handlers.byClassName(nodes, root, className); }, byClassName: function(nodes, root, className) { - if (!nodes) nodes = Selector.handlers.descendant([root]); + if (!nodes) nodes = Prototype.LegacySelector.handlers.descendant([root]); var needle = ' ' + className + ' '; for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) { nodeClassName = node.className; @@ -521,21 +521,21 @@ Object.extend(Selector, { results.push(node); } return results; - }, - + }, + attrPresence: function(nodes, root, attr, combinator) { if (!nodes) nodes = root.getElementsByTagName("*"); if (nodes && combinator) nodes = this[combinator](nodes); var results = []; for (var i = 0, node; node = nodes[i]; i++) if (Element.hasAttribute(node, attr)) results.push(node); - return results; + return results; }, - + attr: function(nodes, root, attr, value, operator, combinator) { if (!nodes) nodes = root.getElementsByTagName("*"); if (nodes && combinator) nodes = this[combinator](nodes); - var handler = Selector.operators[operator], results = []; + var handler = Prototype.LegacySelector.operators[operator], results = []; for (var i = 0, node; node = nodes[i]; i++) { var nodeValue = Element.readAttribute(node, attr); if (nodeValue === null) continue; @@ -543,59 +543,59 @@ Object.extend(Selector, { } return results; }, - + pseudo: function(nodes, name, value, root, combinator) { if (nodes && combinator) nodes = this[combinator](nodes); if (!nodes) nodes = root.getElementsByTagName("*"); - return Selector.pseudos[name](nodes, value, root); + return Prototype.LegacySelector.pseudos[name](nodes, value, root); } }, - + pseudos: { 'first-child': function(nodes, value, root) { for (var i = 0, results = [], node; node = nodes[i]; i++) { - if (Selector.handlers.previousElementSibling(node)) continue; + if (Prototype.LegacySelector.handlers.previousElementSibling(node)) continue; results.push(node); } return results; }, 'last-child': function(nodes, value, root) { for (var i = 0, results = [], node; node = nodes[i]; i++) { - if (Selector.handlers.nextElementSibling(node)) continue; + if (Prototype.LegacySelector.handlers.nextElementSibling(node)) continue; results.push(node); } return results; }, 'only-child': function(nodes, value, root) { - var h = Selector.handlers; + var h = Prototype.LegacySelector.handlers; for (var i = 0, results = [], node; node = nodes[i]; i++) if (!h.previousElementSibling(node) && !h.nextElementSibling(node)) - results.push(node); + results.push(node); return results; }, - 'nth-child': function(nodes, formula, root) { - return Selector.pseudos.nth(nodes, formula, root); - }, - 'nth-last-child': function(nodes, formula, root) { - return Selector.pseudos.nth(nodes, formula, root, true); - }, - 'nth-of-type': function(nodes, formula, root) { - return Selector.pseudos.nth(nodes, formula, root, false, true); - }, - 'nth-last-of-type': function(nodes, formula, root) { - return Selector.pseudos.nth(nodes, formula, root, true, true); - }, - 'first-of-type': function(nodes, formula, root) { - return Selector.pseudos.nth(nodes, "1", root, false, true); - }, - 'last-of-type': function(nodes, formula, root) { - return Selector.pseudos.nth(nodes, "1", root, true, true); + 'nth-child': function(nodes, formula, root) { + return Prototype.LegacySelector.pseudos.nth(nodes, formula, root); + }, + 'nth-last-child': function(nodes, formula, root) { + return Prototype.LegacySelector.pseudos.nth(nodes, formula, root, true); + }, + 'nth-of-type': function(nodes, formula, root) { + return Prototype.LegacySelector.pseudos.nth(nodes, formula, root, false, true); + }, + 'nth-last-of-type': function(nodes, formula, root) { + return Prototype.LegacySelector.pseudos.nth(nodes, formula, root, true, true); + }, + 'first-of-type': function(nodes, formula, root) { + return Prototype.LegacySelector.pseudos.nth(nodes, "1", root, false, true); + }, + 'last-of-type': function(nodes, formula, root) { + return Prototype.LegacySelector.pseudos.nth(nodes, "1", root, true, true); }, 'only-of-type': function(nodes, formula, root) { - var p = Selector.pseudos; + var p = Prototype.LegacySelector.pseudos; return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root); }, - + // handles the an+b logic getIndices: function(a, b, total) { if (a == 0) return b > 0 ? [b] : []; @@ -604,13 +604,13 @@ Object.extend(Selector, { return memo; }); }, - + // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type nth: function(nodes, formula, root, reverse, ofType) { if (nodes.length == 0) return []; if (formula == 'even') formula = '2n+0'; if (formula == 'odd') formula = '2n+1'; - var h = Selector.handlers, results = [], indexed = [], m; + var h = Prototype.LegacySelector.handlers, results = [], indexed = [], m; h.mark(nodes); for (var i = 0, node; node = nodes[i]; i++) { if (!node.parentNode._countedByPrototype) { @@ -626,17 +626,17 @@ Object.extend(Selector, { if (m[1] == "-") m[1] = -1; var a = m[1] ? Number(m[1]) : 1; var b = m[2] ? Number(m[2]) : 0; - var indices = Selector.pseudos.getIndices(a, b, nodes.length); + var indices = Prototype.LegacySelector.pseudos.getIndices(a, b, nodes.length); for (var i = 0, node, l = indices.length; node = nodes[i]; i++) { for (var j = 0; j < l; j++) if (node.nodeIndex == indices[j]) results.push(node); } } h.unmark(nodes); - h.unmark(indexed); - return results; + h.unmark(indexed); + return results; }, - + 'empty': function(nodes, value, root) { for (var i = 0, results = [], node; node = nodes[i]; i++) { // IE treats comments as element nodes @@ -645,50 +645,48 @@ Object.extend(Selector, { } return results; }, - + 'not': function(nodes, selector, root) { - var h = Selector.handlers, selectorType, m; - var exclusions = new Selector(selector).findElements(root); + var h = Prototype.LegacySelector.handlers, selectorType, m; + var exclusions = new Prototype.LegacySelector(selector).findElements(root); h.mark(exclusions); for (var i = 0, results = [], node; node = nodes[i]; i++) if (!node._countedByPrototype) results.push(node); h.unmark(exclusions); return results; }, - + 'enabled': function(nodes, value, root) { for (var i = 0, results = [], node; node = nodes[i]; i++) if (!node.disabled && (!node.type || node.type !== 'hidden')) results.push(node); return results; }, - + 'disabled': function(nodes, value, root) { for (var i = 0, results = [], node; node = nodes[i]; i++) if (node.disabled) results.push(node); return results; }, - + 'checked': function(nodes, value, root) { for (var i = 0, results = [], node; node = nodes[i]; i++) if (node.checked) results.push(node); return results; } }, - + operators: { '=': function(nv, v) { return nv == v; }, '!=': function(nv, v) { return nv != v; }, '^=': function(nv, v) { return nv == v || nv && nv.startsWith(v); }, '$=': function(nv, v) { return nv == v || nv && nv.endsWith(v); }, '*=': function(nv, v) { return nv == v || nv && nv.include(v); }, - '$=': function(nv, v) { return nv.endsWith(v); }, - '*=': function(nv, v) { return nv.include(v); }, '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); }, '|=': function(nv, v) { return ('-' + (nv || "").toUpperCase() + '-').include('-' + (v || "").toUpperCase() + '-'); } }, - + split: function(expression) { var expressions = []; expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) { @@ -698,51 +696,28 @@ Object.extend(Selector, { }, matchElements: function(elements, expression) { - var matches = $$(expression), h = Selector.handlers; + var matches = $$(expression), h = Prototype.LegacySelector.handlers; h.mark(matches); for (var i = 0, results = [], element; element = elements[i]; i++) if (element._countedByPrototype) results.push(element); h.unmark(matches); return results; }, - + findElement: function(elements, expression, index) { - if (Object.isNumber(expression)) { + if (Object.isNumber(expression)) { index = expression; expression = false; } - return Selector.matchElements(elements, expression || '*')[index || 0]; + return Prototype.LegacySelector.matchElements(elements, expression || '*')[index || 0]; }, - + findChildElements: function(element, expressions) { - expressions = Selector.split(expressions.join(',')); - var results = [], h = Selector.handlers; + expressions = Prototype.LegacySelector.split(expressions.join(',')); + var results = [], h = Prototype.LegacySelector.handlers; for (var i = 0, l = expressions.length, selector; i < l; i++) { - selector = new Selector(expressions[i].strip()); + selector = new Prototype.LegacySelector(expressions[i].strip()); h.concat(results, selector.findElements(element)); } return (l > 1) ? h.unique(results) : results; } }); - -if (Prototype.Browser.IE) { - Object.extend(Selector.handlers, { - // IE returns comment nodes on getElementsByTagName("*"). - // Filter them out. - concat: function(a, b) { - for (var i = 0, node; node = b[i]; i++) - if (node.tagName !== "!") a.push(node); - return a; - }, - - // IE improperly serializes _countedByPrototype in (inner|outer)HTML. - unmark: function(nodes) { - for (var i = 0, node; node = nodes[i]; i++) - node.removeAttribute('_countedByPrototype'); - return nodes; - } - }); -} - -function $$() { - return Selector.findChildElements(document, $A(arguments)); -} diff --git a/vendor/legacy_selector/selector_engine.js b/vendor/legacy_selector/selector_engine.js new file mode 100644 index 000000000..3c5cf305f --- /dev/null +++ b/vendor/legacy_selector/selector_engine.js @@ -0,0 +1,15 @@ +//= require "repository/legacy_selector" + +;(function(engine) { + function select(selector, scope) { + return engine.findChildElements(scope || document, [selector]); + } + + function match(element, selector) { + return !!engine.findElement([element], selector); + } + + Prototype.Selector.engine = engine; + Prototype.Selector.select = select; + Prototype.Selector.match = match; +})(Prototype.LegacySelector); diff --git a/vendor/nwmatcher/repository b/vendor/nwmatcher/repository new file mode 160000 index 000000000..c9f5d5d4f --- /dev/null +++ b/vendor/nwmatcher/repository @@ -0,0 +1 @@ +Subproject commit c9f5d5d4fc4ca294477f803bb8d688a8d45de664 diff --git a/vendor/nwmatcher/selector_engine.js b/vendor/nwmatcher/selector_engine.js new file mode 100644 index 000000000..23af2afa8 --- /dev/null +++ b/vendor/nwmatcher/selector_engine.js @@ -0,0 +1,16 @@ +Prototype._original_property = window.NW; +//= require "repository/src/nwmatcher" + +;(function(engine) { + function select(selector, scope) { + return engine.select(selector, scope); + } + + Prototype.Selector.engine = engine; + Prototype.Selector.select = select; + Prototype.Selector.match = engine.match; +})(NW.Dom); + +// Restore globals. +window.NW = Prototype._original_property; +delete Prototype._original_property; diff --git a/vendor/pdoc b/vendor/pdoc index 5d54c7531..b8da8a0e3 160000 --- a/vendor/pdoc +++ b/vendor/pdoc @@ -1 +1 @@ -Subproject commit 5d54c75319969784e7e921ac27a5ef4cb45f50df +Subproject commit b8da8a0e3e33192bf74a0eecdcb88108bf9adcfd diff --git a/vendor/sizzle/repository b/vendor/sizzle/repository new file mode 160000 index 000000000..a9eb3ca3c --- /dev/null +++ b/vendor/sizzle/repository @@ -0,0 +1 @@ +Subproject commit a9eb3ca3c5e1b568057390f73da385809ac69340 diff --git a/vendor/sizzle/selector_engine.js b/vendor/sizzle/selector_engine.js new file mode 100644 index 000000000..40ecfbb16 --- /dev/null +++ b/vendor/sizzle/selector_engine.js @@ -0,0 +1,59 @@ +Prototype._original_property = window.Sizzle; + +;(function () { + function fakeDefine(fn) { + Prototype._actual_sizzle = fn(); + } + fakeDefine.amd = true; + + if (typeof define !== 'undefined' && define.amd) { + // RequireJS is loaded. We need to pretend to be `define` while Sizzle + // runs. + Prototype._original_define = define; + Prototype._actual_sizzle = null; + window.define = fakeDefine; + } +})(); + +//= require "repository/dist/sizzle" + +;(function() { + if (typeof Sizzle !== 'undefined') { + // Sizzle was properly defined. + return; + } + + if (typeof define !== 'undefined' && define.amd) { + // RequireJS. + // We should find Sizzle where we put it. And we need to restore the original `define`. + window.Sizzle = Prototype._actual_sizzle; + window.define = Prototype._original_define; + delete Prototype._actual_sizzle; + delete Prototype._original_define; + // TODO: Should we make our own `define` call here? + } else if (typeof module !== 'undefined' && module.exports) { + // Sizzle saw that it's in a CommonJS environment and attached itself to + // `module.exports` instead. + window.Sizzle = module.exports; + // Reset `module.exports`. + module.exports = {}; + } +})(); + +;(function(engine) { + function select(selector, scope) { + return engine(selector, scope || document); + } + + function match(element, selector) { + return engine.matches(selector, [element]).length == 1; + } + + Prototype.Selector.engine = engine; + Prototype.Selector.select = select; + Prototype.Selector.match = match; +})(window.Sizzle); + +// Restore globals. +window.Sizzle = Prototype._original_property; +delete Prototype._original_property; diff --git a/vendor/slick/repository b/vendor/slick/repository new file mode 160000 index 000000000..b7d3a3305 --- /dev/null +++ b/vendor/slick/repository @@ -0,0 +1 @@ +Subproject commit b7d3a3305675cfaa326c55b3eb1d4c4d4f962744 diff --git a/vendor/slick/selector_engine.js b/vendor/slick/selector_engine.js new file mode 100644 index 000000000..11e5ca1b8 --- /dev/null +++ b/vendor/slick/selector_engine.js @@ -0,0 +1,17 @@ +Prototype._original_property = window.Slick; +//= require "repository/Source/Slick.Parser.js" +//= require "repository/Source/Slick.Finder.js" + +;(function(engine) { + function select(selector, scope) { + return engine.search(scope || document, selector); + } + + Prototype.Selector.engine = engine; + Prototype.Selector.select = select; + Prototype.Selector.match = engine.match; +})(Slick); + +// Restore globals. +window.Slick = Prototype._original_property; +delete Prototype._original_property; diff --git a/vendor/sprockets b/vendor/sprockets index 225b17f7e..8860b7f54 160000 --- a/vendor/sprockets +++ b/vendor/sprockets @@ -1 +1 @@ -Subproject commit 225b17f7ee1926d6e9b63a4e2cddbcd2e534ba01 +Subproject commit 8860b7f54d08dd4861bea1ca5de223555ad4b2c1 diff --git a/vendor/unittest_js b/vendor/unittest_js deleted file mode 160000 index d0d28f58f..000000000 --- a/vendor/unittest_js +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d0d28f58f127796c9cf916bd4f1c95f90b9a3fdc