Compare commits
391 Commits
experiment
...
master
Author | SHA1 | Date |
---|---|---|
John Bintz | a36b6d3e76 | |
John Bintz | 2ede0b24e2 | |
John Bintz | 1c21c2f1b6 | |
John Bintz | 8a5b3209f4 | |
John Bintz | 5992866e46 | |
John Bintz | ef78a973ef | |
Tobie Langel | 6839886699 | |
Juriy Zaytsev | 475f1797e1 | |
Juriy Zaytsev | e0a102b510 | |
Juriy Zaytsev | a260913a30 | |
Tobie Langel | 6c1790ac4e | |
Tobie Langel | 4cc67f2e8e | |
Tobie Langel | 8a8af0a729 | |
Tobie Langel | 2e3e02d92c | |
Tobie Langel | bccb541797 | |
Juriy Zaytsev | 56fb5b84a1 | |
Tobie Langel | d085d97a56 | |
Tobie Langel | fb93b80b59 | |
Tobie Langel | a7f05ee8b5 | |
Juriy Zaytsev | 647d93bcc4 | |
Andrew Dupont | d6ed7efe94 | |
Andrew Dupont | 83b0c153d3 | |
Andrew Dupont | c028935279 | |
Andrew Dupont | 0dd600974c | |
Andrew Dupont | e7c9072872 | |
Sam Stephenson | fa0dce2488 | |
Sam Stephenson | 6bb309afb7 | |
Sam Stephenson | 7770ab99dc | |
Tobie Langel | a44a8db6ae | |
Sam Stephenson | 797c231bfa | |
Tobie Langel | 9d4711281d | |
Samuel Lebeau | 74c5d45511 | |
Tobie Langel | 6b00f24963 | |
Tobie Langel | cdb41a170f | |
Juriy Zaytsev | b3fc07922c | |
Juriy Zaytsev | 18f2ac65c3 | |
Juriy Zaytsev | 82c9b9c783 | |
Juriy Zaytsev | 9ef3f8d2ed | |
Juriy Zaytsev | 829800834d | |
Juriy Zaytsev | c272e40042 | |
Juriy Zaytsev | f40fd5a7d6 | |
Juriy Zaytsev | 1f167f8754 | |
Juriy Zaytsev | 01a229011a | |
Andrew Dupont | 5d027eb939 | |
Tobie Langel | 678774cbd6 | |
Tobie Langel | 74ae0a5537 | |
Tobie Langel | 70c5e98d44 | |
Tobie Langel | 24569d1d98 | |
Tobie Langel | fdf3424f78 | |
Tobie Langel | 107f812525 | |
Tobie Langel | 83826829a7 | |
Tobie Langel | 5f85799c3f | |
Tobie Langel | 75aab03eba | |
Tobie Langel | 7f5ce1e6c2 | |
Tobie Langel | f6f6955a71 | |
Tobie Langel | b0159bdba7 | |
Tobie Langel | af89847a4f | |
Tobie Langel | bf8e404805 | |
Tobie Langel | ed27b225a5 | |
Tobie Langel | 15c323b9ac | |
Tobie Langel | 17e8064d8a | |
Tobie Langel | cba5468b09 | |
Tobie Langel | 7762e002cb | |
Tobie Langel | 3e19f959a2 | |
Tobie Langel | da3e1e361e | |
Tobie Langel | 2d13d45dc8 | |
Tobie Langel | caf66395d5 | |
Yaffle | 2d3e423230 | |
Tobie Langel | f9c680a9ba | |
Tobie Langel | 8783065b8e | |
Tobie Langel | 35ed99ba2e | |
Tobie Langel | 067a0ec364 | |
Andrew Dupont | d3df9ba400 | |
Andrew Dupont | c5372d81f3 | |
Sam Stephenson | 4d042a9fa0 | |
tjcrowder | 859197ca8b | |
tjcrowder | d10aad7bfa | |
tjcrowder | adf80ad1b3 | |
tjcrowder | f1f6fca60b | |
tjcrowder | c448b38f7b | |
tjcrowder | 9ce1ea06b5 | |
tjcrowder | 1e29e3c6c9 | |
tjcrowder | 892eb9d6b3 | |
tjcrowder | e3c89c08c6 | |
tjcrowder | 402a2d408e | |
tjcrowder | 77832408bd | |
tjcrowder | 7df62ce864 | |
tjcrowder | d6d3ab1fef | |
tjcrowder | 7d073ad56a | |
tjcrowder | d2874c0294 | |
Juriy Zaytsev | 3b525f194d | |
Samuel Lebeau | f4ea4c6ef7 | |
Sam Stephenson | 79cf30aab1 | |
Sam Stephenson | 4dd878f237 | |
Sam Stephenson | 9e4a7ce8e8 | |
Andrew Dupont | 4dedcd3b62 | |
Andrew Dupont | 58a2f9db28 | |
Andrew Dupont | 19615e7a00 | |
tjcrowder | cfc7e7a2e7 | |
tjcrowder | f4314a8789 | |
tjcrowder | 63ea557b06 | |
tjcrowder | a47d9e1c2f | |
tjcrowder | e6b6193124 | |
tjcrowder | 20724ea1f9 | |
tjcrowder | b12ec913d7 | |
tjcrowder | e05a089b33 | |
tjcrowder | e5fa0928e8 | |
tjcrowder | 917f10b574 | |
tjcrowder | fe5290e15e | |
tjcrowder | 3ee5b5ddd8 | |
tjcrowder | 1910e08a79 | |
tjcrowder | 71e07f0efa | |
tjcrowder | da402268ac | |
tjcrowder | 48a0f5a44c | |
tjcrowder | 64cd05dd59 | |
tjcrowder | 62a3c8fb38 | |
tjcrowder | ab5a19a1c1 | |
tjcrowder | 305e79e5d3 | |
tjcrowder | e02a1cf5c6 | |
tjcrowder | 00ebde59a2 | |
tjcrowder | f457097afa | |
tjcrowder | 1bab56cfb7 | |
tjcrowder | 4905d1777c | |
tjcrowder | cdaaaa6421 | |
tjcrowder | d441752778 | |
tjcrowder | 037a47d68a | |
tjcrowder | 9300bd0350 | |
tjcrowder | d4aa3b7b4a | |
tjcrowder | 5f02032763 | |
tjcrowder | c7d0bcdb6c | |
tjcrowder | 853e0fbbec | |
tjcrowder | 29b81fda69 | |
Andrew Dupont | 5ccf8cbefd | |
Tobie Langel | cf72372092 | |
tjcrowder | 47c7e12cc6 | |
tjcrowder | 7691177374 | |
tjcrowder | 6a702b420c | |
tjcrowder | 25ae6b44ce | |
tjcrowder | 13dfbaeb38 | |
tjcrowder | cdfd3d5abe | |
tjcrowder | 1a86651055 | |
tjcrowder | 650d49286d | |
tjcrowder | 1c0d5c9a65 | |
tjcrowder | df8ebd727f | |
tjcrowder | ed5456dae0 | |
tjcrowder | 064f673f4b | |
tjcrowder | e5c6fb4272 | |
tjcrowder | adee4abe0c | |
tjcrowder | 45d7e79f4b | |
tjcrowder | 23ebc851b1 | |
tjcrowder | f1c8c1e266 | |
tjcrowder | 061321d17c | |
tjcrowder | 496503a933 | |
Tobie Langel | a7e8e9aa7d | |
tjcrowder | 12b74e6644 | |
tjcrowder | d1cc04c911 | |
tjcrowder | 6b24170e68 | |
tjcrowder | a3eaa0401f | |
tjcrowder | e98c87d681 | |
tjcrowder | d9f0a15dd5 | |
Tobie Langel | a1ab78858f | |
Tobie Langel | 83ad9b622b | |
tjcrowder | 029197572b | |
tjcrowder | 70dbcf39eb | |
tjcrowder | 798a367a3d | |
tjcrowder | a6dac432ac | |
tjcrowder | 7fcd513364 | |
tjcrowder | 53317eae50 | |
tjcrowder | 1bdaaa5f49 | |
Tobie Langel | d98e5c1372 | |
Tobie Langel | 0a8cf9b6a3 | |
Tobie Langel | f8cffdc80b | |
Tobie Langel | f405b2c510 | |
Tobie Langel | 94747c579f | |
Tobie Langel | 4cd37c3d38 | |
Tobie Langel | a0f5af8c43 | |
Tobie Langel | 24141e91c1 | |
Tobie Langel | d5947d9d78 | |
Tobie Langel | ff5dbe343f | |
Tobie Langel | 302e51e7fd | |
tjcrowder | 7735f545c7 | |
tjcrowder | b57bdbd76e | |
tjcrowder | 2763ca4de7 | |
Tobie Langel | 641284a233 | |
Tobie Langel | a4561bda28 | |
tjcrowder | bb20fa0763 | |
tjcrowder | 93b682df47 | |
tjcrowder | 63b7c5d9e8 | |
tjcrowder | 67311e07fd | |
tjcrowder | cc478fcfbd | |
tjcrowder | 41d1088160 | |
tjcrowder | c57c2d270c | |
Tobie Langel | 7cf1182a49 | |
Tobie Langel | 63f96ea463 | |
Tobie Langel | ea9f79728a | |
Tobie Langel | 254f0e9a79 | |
Andrew Dupont | f8ddfd5829 | |
Andrew Dupont | f980f82d41 | |
Andrew Dupont | cdc4366153 | |
Andrew Dupont | 775ea2bf36 | |
Andrew Dupont | 349187e27f | |
Andrew Dupont | e0c03655cd | |
Andrew Dupont | fefc2b728d | |
Tobie Langel | de9ba09371 | |
Tobie Langel | b214744a5d | |
Tobie Langel | b039a4791c | |
Tobie Langel | b57ea83222 | |
Tobie Langel | d88aef3a55 | |
Juriy Zaytsev | 41b034b93b | |
Tobie Langel | add69978e0 | |
Samuel Lebeau | fe388ebca5 | |
Tobie Langel | 49090bc957 | |
Andrew Dupont | e41ccba6d8 | |
Andrew Dupont | 059ce21040 | |
Andrew Dupont | f4570f39d7 | |
Andrew Dupont | 39f045d240 | |
Andrew Dupont | 4586adff0b | |
Andrew Dupont | 1f78f7ebd3 | |
Andrew Dupont | 16b69a2ebf | |
Andrew Dupont | 068f3bba56 | |
Andrew Dupont | a7eff2c0fa | |
Tobie Langel | ad89bfb738 | |
Juriy Zaytsev | 6e4a3cdec8 | |
Juriy Zaytsev | f50dc3639d | |
Juriy Zaytsev | 78f6fad570 | |
Juriy Zaytsev | b2492aeec4 | |
Juriy Zaytsev | d655665fb6 | |
Juriy Zaytsev | 703e988745 | |
Juriy Zaytsev | 7d02aef6ca | |
Juriy Zaytsev | fa4314aeea | |
Andrew Dupont | 1b20386115 | |
Andrew Dupont | 483c7886de | |
Andrew Dupont | 269653fd38 | |
Juriy Zaytsev | 31fe0a0328 | |
Juriy Zaytsev | cebf7d673b | |
Juriy Zaytsev | 74374347c1 | |
Andrew Dupont | c44a071a3f | |
Andrew Dupont | cc0a75fe19 | |
Juriy Zaytsev | cb729625ae | |
Andrew Dupont | 6f6b73cc75 | |
Andrew Dupont | 005f066f1a | |
Andrew Dupont | 37bdd117a0 | |
Andrew Dupont | f0872c96a6 | |
Andrew Dupont | 7bf8bd415f | |
Andrew Dupont | b748b00ce0 | |
Juriy Zaytsev | c480bd7b97 | |
Andrew Dupont | fd46f6bbcf | |
Juriy Zaytsev | 6c38d84254 | |
Juriy Zaytsev | 63bb172663 | |
Juriy Zaytsev | b35ebeb135 | |
Juriy Zaytsev | 3e3b470009 | |
Juriy Zaytsev | d4c182c03d | |
Tobie Langel | b8635ebdde | |
Tobie Langel | f44d0500d9 | |
Tobie Langel | 827e6ee1ae | |
Juriy Zaytsev | 0b4e142d8a | |
Juriy Zaytsev | 71a8663370 | |
Juriy Zaytsev | 2c986d8eaf | |
Juriy Zaytsev | fa15f212fa | |
Juriy Zaytsev | dee8a1010c | |
Juriy Zaytsev | 0e63247604 | |
Juriy Zaytsev | eaa8b949c8 | |
Andrew Dupont | 9bb8fbf2ff | |
Juriy Zaytsev | 06068431f7 | |
Juriy Zaytsev | 30c1935cdb | |
Juriy Zaytsev | 25a38aa717 | |
Juriy Zaytsev | a97c044361 | |
Juriy Zaytsev | 4b9bf985a2 | |
Juriy Zaytsev | 88d7671f0d | |
Juriy Zaytsev | b34355653e | |
Andrew Dupont | e07dcf83df | |
Andrew Dupont | deeb3bbd88 | |
Andrew Dupont | 77db1f9107 | |
Juriy Zaytsev | c7a5d3480e | |
Juriy Zaytsev | 1a375daea2 | |
Andrew Dupont | e0cd7d5edf | |
Andrew Dupont | 4fefe32876 | |
Andrew Dupont | c2915dfe9b | |
Andrew Dupont | 8f697f3e85 | |
Andrew Dupont | fa12b00111 | |
Andrew Dupont | ba2c26090b | |
Andrew Dupont | 7833e8c909 | |
Andrew Dupont | 4453e62fd5 | |
Andrew Dupont | c1a296b79b | |
Andrew Dupont | 0b9997e155 | |
Andrew Dupont | ceae210ab6 | |
Andrew Dupont | 15b936af91 | |
Andrew Dupont | deb3df525e | |
Andrew Dupont | 0f5c9bfcc5 | |
Andrew Dupont | 449e532f9a | |
Andrew Dupont | 8c2af9bd66 | |
Andrew Dupont | 2895be3587 | |
Andrew Dupont | 0445d37116 | |
Andrew Dupont | 205e711ebc | |
Andrew Dupont | d189f809a4 | |
Andrew Dupont | f8e96818f7 | |
Andrew Dupont | 00742ac153 | |
Andrew Dupont | e88f22e0c3 | |
Andrew Dupont | 4a0b9b7c1d | |
Andrew Dupont | 9d7a981e4c | |
Andrew Dupont | 682a55c2d3 | |
Andrew Dupont | 26eaa4300b | |
Andrew Dupont | ee56c93f16 | |
Andrew Dupont | de77550574 | |
Andrew Dupont | d22a9988fa | |
Andrew Dupont | 47abfa68f0 | |
Andrew Dupont | ae667274f7 | |
Andrew Dupont | d97466a7c1 | |
Andrew Dupont | 68096071a4 | |
Andrew Dupont | 8bfd9cf555 | |
Andrew Dupont | 60b9477c5b | |
Andrew Dupont | 7a32414655 | |
Andrew Dupont | 50304a7fa6 | |
Andrew Dupont | c9df079839 | |
Andrew Dupont | 043653a282 | |
Andrew Dupont | e9bdaef0af | |
Andrew Dupont | e3845ba0f6 | |
Andrew Dupont | 432a9422d6 | |
Andrew Dupont | 8fe518719e | |
Andrew Dupont | dc9d274d89 | |
Andrew Dupont | 97fc24a0b2 | |
Andrew Dupont | 9da5045df2 | |
Andrew Dupont | 91b787ae17 | |
Andrew Dupont | ab1313ea20 | |
Andrew Dupont | d3189652a6 | |
Sam Stephenson | 69adbbbf8f | |
Sam Stephenson | 6fb6308744 | |
sstephenson | 463141edc5 | |
James Wheare | 03512f5060 | |
Samuel Lebeau | ceb7b72621 | |
Sam Stephenson | 9bb3d7517d | |
Sam Stephenson | 6d706084ac | |
Sam Stephenson | 938ac16c2d | |
Sam Stephenson | a519c02b2e | |
Sam Stephenson | d5be195340 | |
Andrew Dupont | 952feb48a3 | |
Andrew Dupont | 4af7dc0f5d | |
Andrew Dupont | 44287477cc | |
Andrew Dupont | 9bcf687751 | |
Samuel Lebeau | e819a241e2 | |
Samuel Lebeau | 8a999a0389 | |
Samuel Lebeau | b2597ece0c | |
Andrew Dupont | 24fb692281 | |
Andrew Dupont | 997689fcea | |
savetheclocktower | 31d1c6fd48 | |
Andrew Dupont | 9f5c40c744 | |
Andrew Dupont | e9e8c7fbe5 | |
savetheclocktower | bd1d3fa1ff | |
savetheclocktower | 0549952094 | |
Andrew Dupont | c3c953363b | |
Andrew Dupont | 07506e648b | |
Andrew Dupont | c493d09b7b | |
Andrew Dupont | 60a6c7ac70 | |
savetheclocktower | c4f6066d2c | |
savetheclocktower | 1ca385b18c | |
savetheclocktower | f8a25223d0 | |
Tobie Langel | 2403e3e8ce | |
Andrew Dupont | c4c7bf63db | |
Andrew Dupont | f6a2cdb067 | |
Andrew Dupont | 0f2bbafd11 | |
Andrew Dupont | bc899339a9 | |
Andrew Dupont | 3b21105237 | |
Andrew Dupont | 77b9a2614a | |
Andrew Dupont | 3977e66796 | |
Tobie Langel | 99f74b526a | |
Tobie Langel | 52a781ae2a | |
Tobie Langel | 1d617df4df | |
Tobie Langel | 1cda280a72 | |
Tobie Langel | b74eeeb757 | |
Tobie Langel | 51d0181d29 | |
Tobie Langel | 7f6b3e81a2 | |
Tobie Langel | 1395330440 | |
Tobie Langel | e5f7cac36f | |
Tobie Langel | 86407790d2 | |
Andrew Dupont | 1324e4abe0 | |
Tobie Langel | 2525b21be2 | |
Andrew Dupont | 54bf343560 | |
savetheclocktower | c039f68fb7 | |
Tobie Langel | dad88f6ebf | |
Tobie Langel | 1aacbd692c | |
Tobie Langel | 535d4e6d18 | |
Tobie Langel | 0b646c01c6 | |
Tobie Langel | 48f5ff2072 | |
Tobie Langel | fe35c941eb | |
Tobie Langel | 6850979395 | |
Tobie Langel | 616319377f | |
Tobie Langel | 314e161c57 | |
Tobie Langel | 23b760865a | |
Tobie Langel | 75440aa98d | |
Tobie Langel | 92eac6f1f6 | |
Tobie Langel | d3e833c48e |
|
@ -1,3 +1,5 @@
|
|||
.DS_Store
|
||||
pkg
|
||||
test/unit/tmp/*
|
||||
test/unit/tmp/*
|
||||
doc
|
||||
tmp
|
|
@ -0,0 +1,20 @@
|
|||
[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
|
||||
[submodule "vendor/pdoc"]
|
||||
path = vendor/pdoc
|
||||
url = git://github.com/tobie/pdoc.git
|
||||
[submodule "vendor/sprockets"]
|
||||
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/jeresig/sizzle.git
|
174
CHANGELOG
174
CHANGELOG
|
@ -1,7 +1,177 @@
|
|||
* 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)
|
||||
|
||||
* Switch to Sprockets for building the Prototype distfiles. (sam)
|
||||
|
||||
* Switch Object.is(Array|String|Number) to use the vastly-superior approach discovered by Juriy. (kangax)
|
||||
|
||||
* Further fix to ensure Object.is(String|Number) do not throw exceptions on host objects in IE. (grepmaster, kangax, Tobie Langel, Andrew Dupont)
|
||||
|
||||
* Ensure Enumerable#grep can handle strings with RegExp metacharacters. (Marton Kiss-Albert, kangax)
|
||||
|
||||
* Switch to the "doScroll approach" for the dom:loaded custom event. (javier, Diego Perini, Nick Stakenburg, Andrew Dupont)
|
||||
|
||||
* Optimize document.viewport.get(Dimensions|Width|Height). (Nick Stakenburg, Andrew Dupont)
|
||||
|
||||
* Fix issue where Object#isString and Object#isNumber return false for String and Number "wrapper" objects. (atrepp, Samuel Lebeau, Andrew Dupont)
|
||||
|
||||
* Set document.loaded = true before firing dom:loaded custom event. (Andrew Dupont)
|
||||
|
||||
* Allow Element#store to accept an object containing several key/value pairs. (ZenCocoon, Andrew Dupont)
|
||||
|
||||
* Change Element#store to return the element itself (for chaining). (Andrew Dupont)
|
||||
|
||||
* Add non-bubbling custom events. A new final argument to Element#fire defaults to `true`; pass `false` to prevent bubbling when firing a custom event. (Andrew Dupont)
|
||||
|
||||
* Alter event system to use new element storage API rather than have its own global hashtable. (Andrew Dupont)
|
||||
|
||||
* Add Element#store and Element#retrieve for safe, hash-backed storage of element metadata (no memory leaks). Also add Element#getStorage for working with the element's storage hash directly. Hat tip: Mootools. (ZenCocoon, Andrew Dupont)
|
||||
|
||||
* Fix issue where certain versions of Safari treat class names case-insensitively in Selector/$$ queries. (Andrew Dupont, kangax, Brice)
|
||||
|
||||
* Fix issue where Function#argumentNames returned incorrect results in IE when comments were intermixed with argument names. (Christophe Porteneuve, T.J. Crowder)
|
||||
|
||||
* Selector.patterns should be represented as an ordered structure. (ADO, kangax)
|
||||
|
||||
* Performance improvements in Function methods (Samuel Lebeau, kangax, jddalton, Tobie Langel).
|
||||
|
||||
*1.6.0.3* (September 29, 2008)
|
||||
|
@ -188,13 +358,13 @@
|
|||
|
||||
* Fix the way Selector handles [pseudoclass + combinator] with no space in between. Closes #9696. [kangax, fearphage, Andrew Dupont]
|
||||
|
||||
* Optimize Element#up/down/next/previous. Closes #10353. [Dylan Bruzenak, Nick Stackenburg, Andrew Dupont]
|
||||
* Optimize Element#up/down/next/previous. Closes #10353. [Dylan Bruzenak, Nick Stakenburg, Andrew Dupont]
|
||||
|
||||
* Handle case-sensitivity in Selector tag names better. Closes #5052. [mexx, Andrew Dupont]
|
||||
|
||||
*1.6.0.1* (December 4, 2007)
|
||||
|
||||
* Change document.viewport.getDimensions to exclude scrollbars in all cases. Closes #10148, #9288. [Nick Stackenburg]
|
||||
* Change document.viewport.getDimensions to exclude scrollbars in all cases. Closes #10148, #9288. [Nick Stakenburg]
|
||||
|
||||
* Add logic to Element#getStyle in Opera that fixes inaccurate reporting of computed 'width' and 'height' properties. [Andrew Dupont]
|
||||
|
||||
|
|
|
@ -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 (<http://prototypejs.org/download>) and copy
|
||||
`dist/prototype.js` to a suitable location. Then include it in your HTML
|
||||
like so:
|
||||
|
||||
<script type="text/javascript" src="/path/to/prototype.js"></script>
|
||||
|
||||
### 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 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: <http://prototypejs.org/contribute>.
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
Please see the online Prototype API: <http://api.prototypejs.org>.
|
56
README.rdoc
56
README.rdoc
|
@ -1,56 +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
|
||||
<tt>dist/prototype.js</tt> to a suitable location. Then include it in your HTML
|
||||
like so:
|
||||
|
||||
<script type="text/javascript" src="/path/to/prototype.js"></script>
|
||||
|
||||
=== Building Prototype from source
|
||||
|
||||
<tt>prototype.js</tt> is a composite file generated from many source files in
|
||||
the <tt>src/</tt> 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,
|
||||
|
||||
* <tt>rake dist</tt> will preprocess the Prototype source using ERB and
|
||||
generate the composite <tt>dist/prototype.js</tt>.
|
||||
* <tt>rake package</tt> will create a distribution tarball in the
|
||||
<tt>pkg/</tt> directory.
|
||||
|
||||
== Contributing to Prototype
|
||||
|
||||
Check out the Prototype source with
|
||||
$ git clone git://github.com/sstephenson/prototype.git
|
||||
|
||||
Find out how to contribute:
|
||||
http://prototypejs.org/contribute
|
||||
|
||||
== Documentation
|
||||
|
||||
Please see the online Prototype API:
|
||||
http://prototypejs.org/api
|
||||
|
289
Rakefile
289
Rakefile
|
@ -1,43 +1,183 @@
|
|||
require 'rake'
|
||||
require 'rake/packagetask'
|
||||
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_PKG_DIR = File.join(PROTOTYPE_ROOT, 'pkg')
|
||||
PROTOTYPE_TEST_DIR = File.join(PROTOTYPE_ROOT, 'test')
|
||||
PROTOTYPE_TMP_DIR = File.join(PROTOTYPE_TEST_DIR, 'unit', 'tmp')
|
||||
PROTOTYPE_VERSION = '1.6.0.3'
|
||||
module PrototypeHelper
|
||||
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']
|
||||
DEFAULT_SELECTOR_ENGINE = 'sizzle'
|
||||
|
||||
%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 => e
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
def self.require_git
|
||||
return if has_git?
|
||||
puts "\nPrototype requires Git in order to load its dependencies."
|
||||
puts "\nMake sure you've got Git installed and in your path."
|
||||
puts "\nFor more information, visit:\n\n"
|
||||
puts " http://book.git-scm.com/2_installing_git.html"
|
||||
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)
|
||||
mkdir_p TMP_DIR
|
||||
temp_path = File.join(TMP_DIR, "prototype.temp.js")
|
||||
sprocketize(
|
||||
:path => 'src',
|
||||
:source => file,
|
||||
:destination => temp_path,
|
||||
:selector_engine => ENV['SELECTOR_ENGINE'] || DEFAULT_SELECTOR_ENGINE,
|
||||
:strip_comments => false
|
||||
)
|
||||
rm_rf DOC_DIR
|
||||
|
||||
PDoc.run({
|
||||
:source_files => [temp_path],
|
||||
:destination => DOC_DIR,
|
||||
:index_page => 'README.markdown',
|
||||
:syntax_highlighter => :pygments,
|
||||
:markdown_parser => :bluecloth
|
||||
})
|
||||
|
||||
rm_rf temp_path
|
||||
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 == DEFAULT_SELECTOR_ENGINE || !name
|
||||
submodule_path = File.join(ROOT_DIR, "vendor", name)
|
||||
return submodule_path if File.exist?(File.join(submodule_path, "repository", ".git"))
|
||||
|
||||
get_submodule('the required selector engine', "#{name}/repository")
|
||||
unless File.exist?(submodule_path)
|
||||
puts "The selector engine you required isn't available at vendor/#{name}.\n\n"
|
||||
exit
|
||||
end
|
||||
end
|
||||
|
||||
def self.get_submodule(name, path)
|
||||
require_git
|
||||
puts "\nYou seem to be missing #{name}. Obtaining it via git...\n\n"
|
||||
|
||||
Kernel.system("git submodule init")
|
||||
return true if Kernel.system("git submodule update vendor/#{path}")
|
||||
# If we got this far, something went wrong.
|
||||
puts "\nLooks like it didn't work. Try it manually:\n\n"
|
||||
puts " $ git submodule init"
|
||||
puts " $ git submodule update vendor/#{path}"
|
||||
false
|
||||
end
|
||||
|
||||
def self.require_submodule(name, path)
|
||||
begin
|
||||
require path
|
||||
rescue LoadError => e
|
||||
# Wait until we notice that a submodule is missing before we bother the
|
||||
# user about installing git. (Maybe they brought all the files over
|
||||
# from a different machine.)
|
||||
missing_file = e.message.sub('no such file to load -- ', '')
|
||||
if missing_file == path
|
||||
# Missing 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
|
||||
end
|
||||
|
||||
task :default => [:dist, :dist_helper, :package, :clean_package_source]
|
||||
|
||||
desc "Builds the distribution."
|
||||
task :dist do
|
||||
$:.unshift File.join(PROTOTYPE_ROOT, 'lib')
|
||||
require 'protodoc'
|
||||
PrototypeHelper.sprocketize(
|
||||
:path => 'src',
|
||||
:source => 'prototype.js',
|
||||
:selector_engine => ENV['SELECTOR_ENGINE'] || PrototypeHelper::DEFAULT_SELECTOR_ENGINE
|
||||
)
|
||||
end
|
||||
|
||||
namespace :doc do
|
||||
desc "Builds the documentation."
|
||||
task :build => [:require] do
|
||||
PrototypeHelper.build_doc_for(ENV['SECTION'] ? "#{ENV['SECTION']}.js" : 'prototype.js')
|
||||
end
|
||||
|
||||
Dir.chdir(PROTOTYPE_SRC_DIR) do
|
||||
File.open(File.join(PROTOTYPE_DIST_DIR, 'prototype.js'), 'w+') do |dist|
|
||||
dist << Protodoc::Preprocessor.new('prototype.js')
|
||||
end
|
||||
task :require do
|
||||
PrototypeHelper.require_pdoc
|
||||
end
|
||||
end
|
||||
|
||||
task :doc => ['doc:build']
|
||||
|
||||
desc "Builds the updating helper."
|
||||
task :dist_helper do
|
||||
$:.unshift File.join(PROTOTYPE_ROOT, 'lib')
|
||||
require 'protodoc'
|
||||
|
||||
Dir.chdir(File.join(PROTOTYPE_ROOT, 'ext', 'update_helper')) do
|
||||
File.open(File.join(PROTOTYPE_DIST_DIR, 'prototype_update_helper.js'), 'w+') do |dist|
|
||||
dist << Protodoc::Preprocessor.new('prototype_update_helper.js')
|
||||
end
|
||||
end
|
||||
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',
|
||||
|
@ -47,46 +187,87 @@ Rake::PackageTask.new('prototype', PROTOTYPE_VERSION) do |package|
|
|||
)
|
||||
end
|
||||
|
||||
desc "Builds the distribution and the test suite, runs the tests and collects their results."
|
||||
task :test => [:dist, :test_units]
|
||||
task :clean_package_source do
|
||||
rm_rf File.join(PrototypeHelper::PKG_DIR, "prototype-#{PrototypeHelper::VERSION}")
|
||||
end
|
||||
|
||||
require 'test/lib/jstest'
|
||||
desc "Runs all the JavaScript unit tests and collects the results"
|
||||
JavaScriptTestTask.new(:test_units => [:build_unit_tests]) do |t|
|
||||
testcases = ENV['TESTCASES']
|
||||
tests_to_run = ENV['TESTS'] && ENV['TESTS'].split(',')
|
||||
browsers_to_test = ENV['BROWSERS'] && ENV['BROWSERS'].split(',')
|
||||
|
||||
t.mount("/dist")
|
||||
t.mount("/test")
|
||||
|
||||
Dir.mkdir(PROTOTYPE_TMP_DIR) unless File.exist?(PROTOTYPE_TMP_DIR)
|
||||
|
||||
Dir["test/unit/tmp/*_test.html"].each do |file|
|
||||
test_name = File.basename(file).sub("_test.html", "")
|
||||
unless tests_to_run && !tests_to_run.include?(test_name)
|
||||
t.run("/#{file}", testcases)
|
||||
task :test => ['test:build', 'test:run']
|
||||
namespace :test do
|
||||
desc 'Runs all the JavaScript unit tests and collects the results'
|
||||
task :run => [:require] do
|
||||
testcases = ENV['TESTCASES']
|
||||
browsers_to_test = ENV['BROWSERS'] && ENV['BROWSERS'].split(',')
|
||||
tests_to_run = ENV['TESTS'] && ENV['TESTS'].split(',')
|
||||
runner = UnittestJS::WEBrickRunner::Runner.new(:test_dir => PrototypeHelper::TMP_DIR)
|
||||
|
||||
Dir[File.join(PrototypeHelper::TMP_DIR, '*_test.html')].each do |file|
|
||||
file = File.basename(file)
|
||||
test = file.sub('_test.html', '')
|
||||
unless tests_to_run && !tests_to_run.include?(test)
|
||||
runner.add_test(file, testcases)
|
||||
end
|
||||
end
|
||||
|
||||
UnittestJS::Browser::SUPPORTED.each do |browser|
|
||||
unless browsers_to_test && !browsers_to_test.include?(browser)
|
||||
runner.add_browser(browser.to_sym)
|
||||
end
|
||||
end
|
||||
|
||||
trap('INT') { runner.teardown; exit }
|
||||
runner.run
|
||||
end
|
||||
|
||||
%w( safari firefox ie konqueror opera chrome ).each do |browser|
|
||||
t.browser(browser.to_sym) unless browsers_to_test && !browsers_to_test.include?(browser)
|
||||
task :build => [:clean, :dist] do
|
||||
builder = UnittestJS::Builder::SuiteBuilder.new({
|
||||
:input_dir => PrototypeHelper::TEST_UNIT_DIR,
|
||||
:assets_dir => PrototypeHelper::DIST_DIR
|
||||
})
|
||||
selected_tests = (ENV['TESTS'] || '').split(',')
|
||||
builder.collect(*selected_tests)
|
||||
builder.render
|
||||
end
|
||||
|
||||
task :clean => [:require] do
|
||||
UnittestJS::Builder.empty_dir!(PrototypeHelper::TMP_DIR)
|
||||
end
|
||||
|
||||
task :require do
|
||||
PrototypeHelper.require_unittest_js
|
||||
end
|
||||
end
|
||||
|
||||
task :test_units do
|
||||
puts '"rake test_units" is deprecated. Please use "rake test" instead.'
|
||||
end
|
||||
|
||||
task :build_unit_tests do
|
||||
Dir[File.join('test', 'unit', '*_test.js')].each do |file|
|
||||
PageBuilder.new(file, 'prototype.erb').render
|
||||
end
|
||||
puts '"rake test_units" is deprecated. Please use "rake test:build" instead.'
|
||||
end
|
||||
|
||||
task :clean_package_source do
|
||||
rm_rf File.join(PROTOTYPE_PKG_DIR, "prototype-#{PROTOTYPE_VERSION}")
|
||||
end
|
||||
|
||||
desc 'Generates an empty tmp directory for building tests.'
|
||||
task :clean_tmp do
|
||||
puts 'Generating an empty tmp directory for building tests.'
|
||||
FileUtils.rm_rf(PROTOTYPE_TMP_DIR) if File.exist?(PROTOTYPE_TMP_DIR)
|
||||
Dir.mkdir(PROTOTYPE_TMP_DIR)
|
||||
puts '"rake clean_tmp" is deprecated. Please use "rake test:clean" instead.'
|
||||
end
|
||||
|
||||
namespace :caja do
|
||||
task :test => ['test:build', 'test:run']
|
||||
|
||||
namespace :test do
|
||||
task :run => ['rake:test:run']
|
||||
|
||||
task :build => [:require, 'rake:test:clean', :dist] do
|
||||
builder = UnittestJS::CajaBuilder::SuiteBuilder.new({
|
||||
:input_dir => PrototypeHelper::TEST_UNIT_DIR,
|
||||
:assets_dir => PrototypeHelper::DIST_DIR,
|
||||
:whitelist_dir => File.join(PrototypeHelper::TEST_DIR, 'unit', 'caja_whitelists'),
|
||||
:html_attrib_schema => 'html_attrib.json'
|
||||
})
|
||||
selected_tests = (ENV['TESTS'] || '').split(',')
|
||||
builder.collect(*selected_tests)
|
||||
builder.render
|
||||
end
|
||||
end
|
||||
task :require => ['rake:test:require'] do
|
||||
PrototypeHelper.require_caja_builder
|
||||
end
|
||||
end
|
|
@ -6,8 +6,8 @@
|
|||
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||
<script src="../../dist/prototype.js" type="text/javascript"></script>
|
||||
<script src="../../dist/prototype_update_helper.js" type="text/javascript"></script>
|
||||
<script src="../../test/lib/unittest.js" type="text/javascript"></script>
|
||||
<link rel="stylesheet" href="../../test/test.css" type="text/css" />
|
||||
<script src="../../vendor/unittest_js/assets/unittest.js" type="text/javascript" charset="utf-8"></script>
|
||||
<link rel="stylesheet" href="../../vendor/unittest_js/assets/unittest.css" type="text/css" charset="utf-8" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>Prototype Unit test file</h1>
|
||||
|
@ -45,8 +45,8 @@
|
|||
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 @@
|
|||
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 @@
|
|||
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 @@
|
|||
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 @@
|
|||
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 });
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<%= include "update_helper.js" %>
|
||||
//= 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);
|
||||
});
|
||||
|
|
|
@ -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 ''; }
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
require 'erb'
|
||||
|
||||
class String
|
||||
def lines
|
||||
split $/
|
||||
end
|
||||
|
||||
def strip_whitespace_at_line_ends
|
||||
lines.map {|line| line.gsub(/\s+$/, '')} * $/
|
||||
end
|
||||
end
|
||||
|
||||
module Protodoc
|
||||
module Environment
|
||||
def include(*filenames)
|
||||
filenames.map {|filename| Preprocessor.new(filename).to_s}.join("\n")
|
||||
end
|
||||
end
|
||||
|
||||
class Preprocessor
|
||||
include Environment
|
||||
|
||||
def initialize(filename)
|
||||
filename = File.join(filename.split('/'))
|
||||
@filename = File.expand_path(filename)
|
||||
@template = ERB.new(IO.read(@filename), nil, '%')
|
||||
end
|
||||
|
||||
def to_s
|
||||
@template.result(binding).strip_whitespace_at_line_ends
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if __FILE__ == $0
|
||||
print Protodoc::Preprocessor.new(ARGV.first)
|
||||
end
|
|
@ -1,7 +0,0 @@
|
|||
/* Prototype JavaScript framework, version <%= PROTOTYPE_VERSION %>
|
||||
* (c) 2005-2008 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/
|
||||
*
|
||||
*--------------------------------------------------------------------------*/
|
|
@ -0,0 +1,116 @@
|
|||
/**
|
||||
* == 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]].
|
||||
*
|
||||
* <h5>Request headers</h5>
|
||||
*
|
||||
* 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.,
|
||||
* `1.6.0.3`).
|
||||
* * `Accept` is set to `text/javascript, text/html, application/xml,
|
||||
* text/xml, * / *`
|
||||
* * `Content-type` is automatically determined based on the `contentType`
|
||||
* and `encoding` options.
|
||||
*
|
||||
* <h5>Ajax options</h5>
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* <h5>Common options</h5>
|
||||
*
|
||||
* * `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.
|
||||
* * `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]]): Specific contents for the request body on a
|
||||
* `post` method. If it is not provided, the contents of the `parameters`
|
||||
* option will be used instead.
|
||||
* * `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]] 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.
|
||||
*
|
||||
* <h5>Common callbacks</h5>
|
||||
*
|
||||
* When used on individual instances, all callbacks (except `onException`) are
|
||||
* invoked with two parameters: the `XMLHttpRequest` 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.
|
||||
* * `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.
|
||||
*
|
||||
**/
|
||||
|
||||
//= require "ajax/ajax"
|
||||
//= require "ajax/responders"
|
||||
//= require "ajax/base"
|
||||
//= require "ajax/request"
|
||||
//= require "ajax/response"
|
||||
//= require "ajax/updater"
|
||||
//= require "ajax/periodical_updater"
|
|
@ -1,3 +1,7 @@
|
|||
/** section: Ajax
|
||||
* Ajax
|
||||
**/
|
||||
|
||||
var Ajax = {
|
||||
getTransport: function() {
|
||||
return Try.these(
|
||||
|
@ -6,6 +10,12 @@ var Ajax = {
|
|||
function() {return new ActiveXObject('Microsoft.XMLHTTP')}
|
||||
) || false;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Ajax.activeRequestCount -> Number
|
||||
*
|
||||
* Represents the number of active XHR requests triggered through
|
||||
* [[Ajax.Request]], [[Ajax.Updater]], or [[Ajax.PeriodicalUpdater]].
|
||||
**/
|
||||
activeRequestCount: 0
|
||||
};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// Abstract class; does not need documentation.
|
||||
Ajax.Base = Class.create({
|
||||
initialize: function(options) {
|
||||
this.options = {
|
||||
|
@ -10,10 +11,10 @@ Ajax.Base = Class.create({
|
|||
evalJS: true
|
||||
};
|
||||
Object.extend(this.options, options || { });
|
||||
|
||||
|
||||
this.options.method = this.options.method.toLowerCase();
|
||||
|
||||
if (Object.isString(this.options.parameters))
|
||||
|
||||
if (Object.isString(this.options.parameters))
|
||||
this.options.parameters = this.options.parameters.toQueryParams();
|
||||
else if (Object.isHash(this.options.parameters))
|
||||
this.options.parameters = this.options.parameters.toObject();
|
||||
|
|
|
@ -1,11 +1,69 @@
|
|||
/** 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.
|
||||
*
|
||||
* <h5>Additional options</h5>
|
||||
*
|
||||
* `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.
|
||||
*
|
||||
* <h5>Disabling and re-enabling a <code>PeriodicalUpdater</code></h5>
|
||||
*
|
||||
* You can hit the brakes on a running `PeriodicalUpdater` by calling
|
||||
* [[Ajax.PeriodicalUpdater#stop]]. If you wish to re-enable it later, call
|
||||
* [[Ajax.PeriodicalUpdater#start]].
|
||||
*
|
||||
**/
|
||||
|
||||
Ajax.PeriodicalUpdater = Class.create(Ajax.Base, {
|
||||
/**
|
||||
* new Ajax.PeriodicalUpdater(container, url[, options])
|
||||
* - container (String | Element): The DOM element whose contents to update
|
||||
* as a result of the Ajax request. Can be a DOM node or a string that
|
||||
* identifies a node's ID.
|
||||
* - url (String): The URL to fetch. When the _same-origin_ policy is in
|
||||
* effect (as it is in most cases), `url` **must** be a relative URL or an
|
||||
* absolute URL that starts with a slash (i.e., it must not begin with
|
||||
* `http`).
|
||||
* - options (Object): Configuration for the request. See the
|
||||
* [[Ajax section]] for more information.
|
||||
*
|
||||
* Creates a new `Ajax.PeriodicalUpdater`.
|
||||
**/
|
||||
initialize: function($super, container, url, options) {
|
||||
$super(options);
|
||||
this.onComplete = this.options.onComplete;
|
||||
|
||||
this.frequency = (this.options.frequency || 2);
|
||||
this.decay = (this.options.decay || 1);
|
||||
|
||||
|
||||
this.updater = { };
|
||||
this.container = container;
|
||||
this.url = url;
|
||||
|
@ -13,11 +71,24 @@ Ajax.PeriodicalUpdater = Class.create(Ajax.Base, {
|
|||
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);
|
||||
|
@ -26,7 +97,7 @@ Ajax.PeriodicalUpdater = Class.create(Ajax.Base, {
|
|||
|
||||
updateComplete: function(response) {
|
||||
if (this.options.decay) {
|
||||
this.decay = (response.responseText == this.lastText ?
|
||||
this.decay = (response.responseText == this.lastText ?
|
||||
this.decay * this.options.decay : 1);
|
||||
|
||||
this.lastText = response.responseText;
|
||||
|
|
|
@ -1,6 +1,92 @@
|
|||
/** section: Ajax
|
||||
* class Ajax.Request
|
||||
*
|
||||
* Initiates and processes an Ajax request.
|
||||
*
|
||||
* `Ajax.Request` is a general-purpose class for making HTTP requests.
|
||||
*
|
||||
* <h5>Automatic JavaScript response evaluation</h5>
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* <h5>Methods you may find useful</h5>
|
||||
*
|
||||
* Instances of the `Request` object provide several methods that can come in
|
||||
* handy in your callback functions, especially once the request is complete.
|
||||
*
|
||||
* <h5>Is the response a successful one?</h5>
|
||||
*
|
||||
* 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`.
|
||||
*
|
||||
* <h5>Getting HTTP response headers</h5>
|
||||
*
|
||||
* 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
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* <h5>Evaluating JSON headers</h5>
|
||||
*
|
||||
* 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();
|
||||
|
@ -17,7 +103,7 @@ Ajax.Request = Class.create(Ajax.Base, {
|
|||
params['_method'] = this.method;
|
||||
this.method = 'post';
|
||||
}
|
||||
|
||||
|
||||
this.parameters = params;
|
||||
|
||||
if (params = Object.toQueryString(params)) {
|
||||
|
@ -27,17 +113,17 @@ Ajax.Request = Class.create(Ajax.Base, {
|
|||
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.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();
|
||||
|
||||
|
@ -47,7 +133,7 @@ Ajax.Request = Class.create(Ajax.Base, {
|
|||
/* Force Firefox to handle ready state 4 for synchronous requests */
|
||||
if (!this.options.asynchronous && this.transport.overrideMimeType)
|
||||
this.onStateChange();
|
||||
|
||||
|
||||
}
|
||||
catch (e) {
|
||||
this.dispatchException(e);
|
||||
|
@ -59,7 +145,7 @@ Ajax.Request = Class.create(Ajax.Base, {
|
|||
if (readyState > 1 && !((readyState == 4) && this._complete))
|
||||
this.respondToReadyState(this.transport.readyState);
|
||||
},
|
||||
|
||||
|
||||
setRequestHeaders: function() {
|
||||
var headers = {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
|
@ -70,42 +156,47 @@ Ajax.Request = Class.create(Ajax.Base, {
|
|||
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.
|
||||
* 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)
|
||||
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)
|
||||
for (var name in headers)
|
||||
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);
|
||||
},
|
||||
|
||||
|
||||
getStatus: function() {
|
||||
try {
|
||||
return this.transport.status || 0;
|
||||
} catch (e) { return 0 }
|
||||
} catch (e) { return 0 }
|
||||
},
|
||||
|
||||
|
||||
respondToReadyState: function(readyState) {
|
||||
var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this);
|
||||
|
||||
|
@ -118,10 +209,10 @@ Ajax.Request = Class.create(Ajax.Base, {
|
|||
} catch (e) {
|
||||
this.dispatchException(e);
|
||||
}
|
||||
|
||||
|
||||
var contentType = response.getHeader('Content-type');
|
||||
if (this.options.evalJS == 'force'
|
||||
|| (this.options.evalJS && this.isSameOrigin() && contentType
|
||||
|| (this.options.evalJS && this.isSameOrigin() && contentType
|
||||
&& contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i)))
|
||||
this.evalResponse();
|
||||
}
|
||||
|
@ -132,13 +223,13 @@ Ajax.Request = Class.create(Ajax.Base, {
|
|||
} 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({
|
||||
|
@ -147,13 +238,21 @@ Ajax.Request = Class.create(Ajax.Base, {
|
|||
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 }
|
||||
} catch (e) { return null; }
|
||||
},
|
||||
|
||||
|
||||
evalResponse: function() {
|
||||
try {
|
||||
return eval((this.transport.responseText || '').unfilterJSON());
|
||||
|
@ -168,5 +267,5 @@ Ajax.Request = Class.create(Ajax.Base, {
|
|||
}
|
||||
});
|
||||
|
||||
Ajax.Request.Events =
|
||||
Ajax.Request.Events =
|
||||
['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
|
||||
|
|
|
@ -1,19 +1,107 @@
|
|||
/** 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--;
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* <h5>Responder callbacks</h5>
|
||||
*
|
||||
* 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) {
|
||||
this.responders._each(iterator);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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])) {
|
||||
|
@ -28,6 +116,6 @@ Ajax.Responders = {
|
|||
Object.extend(Ajax.Responders, Enumerable);
|
||||
|
||||
Ajax.Responders.register({
|
||||
onCreate: function() { Ajax.activeRequestCount++ },
|
||||
onCreate: function() { Ajax.activeRequestCount++ },
|
||||
onComplete: function() { Ajax.activeRequestCount-- }
|
||||
});
|
||||
|
|
|
@ -1,50 +1,147 @@
|
|||
/** 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
|
||||
*
|
||||
* The request's current state.
|
||||
*
|
||||
* `0` corresponds to `"Uninitialized"`, `1` to `"Loading"`, `2` to
|
||||
* `"Loaded"`, `3` to `"Interactive"`, and `4` to `"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.
|
||||
**/
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
|
||||
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) {
|
||||
|
||||
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 }
|
||||
} 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.
|
||||
**/
|
||||
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.
|
||||
**/
|
||||
getAllResponseHeaders: function() {
|
||||
return this.transport.getAllResponseHeaders();
|
||||
},
|
||||
|
||||
|
||||
_getHeaderJSON: function() {
|
||||
var json = this.getHeader('X-JSON');
|
||||
if (!json) return null;
|
||||
|
@ -56,11 +153,11 @@ Ajax.Response = Class.create({
|
|||
this.request.dispatchException(e);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
_getResponseJSON: function() {
|
||||
var options = this.request.options;
|
||||
if (!options.evalJSON || (options.evalJSON != 'force' &&
|
||||
!(this.getHeader('Content-type') || '').include('application/json')) ||
|
||||
if (!options.evalJSON || (options.evalJSON != 'force' &&
|
||||
!(this.getHeader('Content-type') || '').include('application/json')) ||
|
||||
this.responseText.blank())
|
||||
return null;
|
||||
try {
|
||||
|
|
|
@ -1,4 +1,96 @@
|
|||
/** 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.
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* <h5>Callbacks</h5>
|
||||
*
|
||||
* `Ajax.Updater` supports all the callbacks listed in the [[Ajax section]].
|
||||
* Note that the `onComplete` callback will be invoked **after** the element
|
||||
* is updated.
|
||||
*
|
||||
* <h5>Additional options</h5>
|
||||
*
|
||||
* `Ajax.Updater` has some options of its own apart from the common options
|
||||
* described in the [[Ajax section]]:
|
||||
*
|
||||
* * `evalScripts` ([[Boolean]]; defaults to `false`): Whether `<script>`
|
||||
* elements in the response text should be evaluated.
|
||||
* * `insertion` ([[String]]): By default, `Element.update` is used, meaning
|
||||
* the contents of the response will replace the entire contents of the
|
||||
* container. You may _instead_ insert the response text without disrupting
|
||||
* existing contents. The `insertion` option takes one of four strings —
|
||||
* `top`, `bottom`, `before`, or `after` — and _inserts_ the contents of the
|
||||
* response in the manner described by [[Element#insert]].
|
||||
*
|
||||
* <h5>More About `evalScripts`</h5>
|
||||
*
|
||||
* If you use `evalScripts: true`, any _inline_ `<script>` block will be evaluated.
|
||||
* This **does not** mean it will be evaluated in the global scope; it won't, and that
|
||||
* has important ramifications for your `var` and `function` statements. Also note
|
||||
* that only inline `<script>` blocks are supported; external scripts are ignored.
|
||||
* See [[String#evalScripts]] for the details.
|
||||
*
|
||||
* <h5>Single container, or success/failure split?</h5>
|
||||
*
|
||||
* The examples above all assume you're going to update the same container
|
||||
* whether your request succeeds or fails. Instead, you may want to update
|
||||
* _only_ for successful requests, or update a _different container_ on failed
|
||||
* requests.
|
||||
*
|
||||
* To achieve this, you can pass an object instead of a DOM element for the
|
||||
* `container` parameter. This object _must_ have a `success` property whose
|
||||
* value identifies the container to be updated on successful requests.
|
||||
*
|
||||
* If you also provide it with a `failure` property, its value will be used as
|
||||
* the container for failed requests.
|
||||
*
|
||||
* In the following code, only successful requests get an update:
|
||||
*
|
||||
* new Ajax.Updater({ success: 'items' }, '/items', {
|
||||
* parameters: { text: $F('text') },
|
||||
* insertion: 'bottom'
|
||||
* });
|
||||
*
|
||||
* This next example assumes failed requests will deliver an error message as
|
||||
* response text — one that should be shown to the user in another area:
|
||||
*
|
||||
* new Ajax.Updater({ success: 'items', failure: 'notice' }, '/items',
|
||||
* parameters: { text: $F('text') },
|
||||
* insertion: 'bottom'
|
||||
* });
|
||||
*
|
||||
**/
|
||||
|
||||
Ajax.Updater = Class.create(Ajax.Request, {
|
||||
/**
|
||||
* new Ajax.Updater(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.Updater`.
|
||||
**/
|
||||
initialize: function($super, container, url, options) {
|
||||
this.container = {
|
||||
success: (container.success || container),
|
||||
|
@ -16,11 +108,11 @@ Ajax.Updater = Class.create(Ajax.Request, {
|
|||
},
|
||||
|
||||
updateContent: function(responseText) {
|
||||
var receiver = this.container[this.success() ? 'success' : 'failure'],
|
||||
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)) {
|
||||
|
@ -28,7 +120,7 @@ Ajax.Updater = Class.create(Ajax.Request, {
|
|||
receiver.insert(insertion);
|
||||
}
|
||||
else options.insertion(receiver, responseText);
|
||||
}
|
||||
}
|
||||
else receiver.update(responseText);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
PROTOTYPE_VERSION: 1.6.1
|
|
@ -10,15 +10,15 @@ var Insertion = {
|
|||
Before: function(element, content) {
|
||||
return Element.insert(element, {before:content});
|
||||
},
|
||||
|
||||
|
||||
Top: function(element, content) {
|
||||
return Element.insert(element, {top:content});
|
||||
},
|
||||
|
||||
|
||||
Bottom: function(element, content) {
|
||||
return Element.insert(element, {bottom:content});
|
||||
},
|
||||
|
||||
|
||||
After: function(element, content) {
|
||||
return Element.insert(element, {after:content});
|
||||
}
|
||||
|
@ -32,21 +32,21 @@ var Position = {
|
|||
// set to true if needed, warning: firefox performance problems
|
||||
// NOT neeeded for page scrolling, only if draggable contained in
|
||||
// scrollable elements
|
||||
includeScrollOffsets: false,
|
||||
includeScrollOffsets: false,
|
||||
|
||||
// must be called before calling withinIncludingScrolloffset, every time the
|
||||
// page is scrolled
|
||||
prepare: function() {
|
||||
this.deltaX = window.pageXOffset
|
||||
|| document.documentElement.scrollLeft
|
||||
|| document.body.scrollLeft
|
||||
this.deltaX = window.pageXOffset
|
||||
|| document.documentElement.scrollLeft
|
||||
|| document.body.scrollLeft
|
||||
|| 0;
|
||||
this.deltaY = window.pageYOffset
|
||||
|| document.documentElement.scrollTop
|
||||
|| document.body.scrollTop
|
||||
this.deltaY = window.pageYOffset
|
||||
|| document.documentElement.scrollTop
|
||||
|| document.body.scrollTop
|
||||
|| 0;
|
||||
},
|
||||
|
||||
|
||||
// caches x/y coordinate pair to use with overlap
|
||||
within: function(element, x, y) {
|
||||
if (this.includeScrollOffsets)
|
||||
|
@ -57,7 +57,7 @@ var Position = {
|
|||
|
||||
return (y >= this.offset[1] &&
|
||||
y < this.offset[1] + element.offsetHeight &&
|
||||
x >= this.offset[0] &&
|
||||
x >= this.offset[0] &&
|
||||
x < this.offset[0] + element.offsetWidth);
|
||||
},
|
||||
|
||||
|
@ -70,18 +70,18 @@ var Position = {
|
|||
|
||||
return (this.ycomp >= this.offset[1] &&
|
||||
this.ycomp < this.offset[1] + element.offsetHeight &&
|
||||
this.xcomp >= this.offset[0] &&
|
||||
this.xcomp >= this.offset[0] &&
|
||||
this.xcomp < this.offset[0] + element.offsetWidth);
|
||||
},
|
||||
|
||||
// within must be called directly before
|
||||
overlap: function(mode, element) {
|
||||
if (!mode) return 0;
|
||||
if (mode == 'vertical')
|
||||
return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
|
||||
overlap: function(mode, element) {
|
||||
if (!mode) return 0;
|
||||
if (mode == 'vertical')
|
||||
return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
|
||||
element.offsetHeight;
|
||||
if (mode == 'horizontal')
|
||||
return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
|
||||
return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
|
||||
element.offsetWidth;
|
||||
},
|
||||
|
||||
|
@ -161,21 +161,21 @@ Element.ClassNames.prototype = {
|
|||
return name.length > 0;
|
||||
})._each(iterator);
|
||||
},
|
||||
|
||||
|
||||
set: function(className) {
|
||||
this.element.className = className;
|
||||
},
|
||||
|
||||
|
||||
add: function(classNameToAdd) {
|
||||
if (this.include(classNameToAdd)) return;
|
||||
this.set($A(this).concat(classNameToAdd).join(' '));
|
||||
},
|
||||
|
||||
|
||||
remove: function(classNameToRemove) {
|
||||
if (!this.include(classNameToRemove)) return;
|
||||
this.set($A(this).without(classNameToRemove).join(' '));
|
||||
},
|
||||
|
||||
|
||||
toString: function() {
|
||||
return $A(this).join(' ');
|
||||
}
|
||||
|
@ -184,3 +184,95 @@ Element.ClassNames.prototype = {
|
|||
Object.extend(Element.ClassNames.prototype, Enumerable);
|
||||
|
||||
/*--------------------------------------------------------------------------*/
|
||||
|
||||
/** deprecated, section: DOM
|
||||
* class Selector
|
||||
*
|
||||
* A class that queries the document for elements that match a given CSS
|
||||
* selector.
|
||||
**/
|
||||
(function() {
|
||||
window.Selector = Class.create({
|
||||
/** deprecated
|
||||
* new Selector(expression)
|
||||
* - expression (String): A CSS selector.
|
||||
*
|
||||
* Creates a `Selector` with the given CSS selector.
|
||||
**/
|
||||
initialize: function(expression) {
|
||||
this.expression = expression.strip();
|
||||
},
|
||||
|
||||
/** deprecated
|
||||
* Selector#findElements(root) -> [Element...]
|
||||
* - root (Element | document): A "scope" to search within. All results will
|
||||
* be descendants of this node.
|
||||
*
|
||||
* Searches the document for elements that match the instance's CSS
|
||||
* selector.
|
||||
**/
|
||||
findElements: function(rootElement) {
|
||||
return Prototype.Selector.select(this.expression, rootElement);
|
||||
},
|
||||
|
||||
/** deprecated
|
||||
* Selector#match(element) -> Boolean
|
||||
*
|
||||
* Tests whether a `element` matches the instance's CSS selector.
|
||||
**/
|
||||
match: function(element) {
|
||||
return Prototype.Selector.match(element, this.expression);
|
||||
},
|
||||
|
||||
toString: function() {
|
||||
return this.expression;
|
||||
},
|
||||
|
||||
inspect: function() {
|
||||
return "#<Selector: " + this.expression + ">";
|
||||
}
|
||||
});
|
||||
|
||||
Object.extend(Selector, {
|
||||
/** deprecated
|
||||
* Selector.matchElements(elements, expression) -> [Element...]
|
||||
*
|
||||
* Filters the given collection of elements with `expression`.
|
||||
*
|
||||
* The only nodes returned will be those that match the given CSS selector.
|
||||
**/
|
||||
matchElements: Prototype.Selector.filter,
|
||||
|
||||
/** deprecated
|
||||
* Selector.findElement(elements, expression[, index = 0]) -> Element
|
||||
* Selector.findElement(elements[, index = 0]) -> Element
|
||||
*
|
||||
* Returns the `index`th element in the collection that matches
|
||||
* `expression`.
|
||||
*
|
||||
* Returns the `index`th element overall if `expression` is not given.
|
||||
**/
|
||||
findElement: function(elements, expression, index) {
|
||||
index = index || 0;
|
||||
var matchIndex = 0, element;
|
||||
// Match each element individually, since Sizzle.matches does not preserve order
|
||||
for (var i = 0, length = elements.length; i < length; i++) {
|
||||
element = elements[i];
|
||||
if (Prototype.Selector.match(element, expression) && index === matchIndex++) {
|
||||
return Element.extend(element);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/** deprecated
|
||||
* Selector.findChildElements(element, expressions) -> [Element...]
|
||||
*
|
||||
* Searches beneath `element` for any elements that match the selector
|
||||
* (or selectors) specified in `expressions`.
|
||||
**/
|
||||
findChildElements: function(element, expressions) {
|
||||
var selector = expressions.toArray().join(', ');
|
||||
return Prototype.Selector.select(selector, element || document);
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* == DOM ==
|
||||
* Extensions to DOM elements, plus other utilities for DOM traversal
|
||||
* and modification.
|
||||
*
|
||||
* Prototype's DOM extensions represent a large portion of where you'll spend
|
||||
* your time. Prototype adds many convenience methods to elements returned by
|
||||
* the [[$]] function. For instance, you can write
|
||||
*
|
||||
* $('comments').addClassName('active').show();
|
||||
*
|
||||
* to get the element with the ID of `comments`, add a class name to it, and
|
||||
* show it (if it was previously hidden).
|
||||
*
|
||||
* In other words, Prototype adds "instance" methods to DOM nodes. This is
|
||||
* made possible by direct extension of the backing DOM objects (in browsers
|
||||
* that support it) and by manual extension of individual nodes (in browsers
|
||||
* that do not).
|
||||
*
|
||||
**/
|
||||
|
||||
/** section: DOM
|
||||
* Prototype
|
||||
*
|
||||
* The Prototype namespace.
|
||||
*
|
||||
**/
|
||||
|
||||
//= require "dom/dom"
|
||||
//= require <selector_engine>
|
||||
//= require "dom/selector"
|
||||
//= require "dom/form"
|
||||
//= require "dom/event"
|
||||
|
||||
Element.addMethods();
|
1932
src/dom/dom.js
1932
src/dom/dom.js
File diff suppressed because it is too large
Load Diff
690
src/dom/event.js
690
src/dom/event.js
|
@ -1,4 +1,39 @@
|
|||
(function() {
|
||||
|
||||
/** section: DOM
|
||||
* class Event
|
||||
*
|
||||
* The namespace for Prototype's event system.
|
||||
*
|
||||
* <h5>Events: a fine mess</h5>
|
||||
*
|
||||
* Event management is one of the really sore spots of cross-browser
|
||||
* scripting.
|
||||
*
|
||||
* True, the prevalent issue is: everybody does it the W3C way, and MSIE
|
||||
* does it another way altogether. But there are quite a few subtler,
|
||||
* sneakier issues here and there waiting to bite your ankle — such as the
|
||||
* `keypress`/`keydown` issue with KHTML-based browsers (Konqueror and
|
||||
* Safari). Also, MSIE has a tendency to leak memory when it comes to
|
||||
* discarding event handlers.
|
||||
*
|
||||
* <h5>Prototype to the rescue</h5>
|
||||
*
|
||||
* Of course, Prototype smooths it over so well you'll forget these
|
||||
* troubles even exist. Enter the `Event` namespace. It is replete with
|
||||
* methods that help to normalize the information reported by events across
|
||||
* browsers.
|
||||
*
|
||||
* `Event` also provides a standardized list of key codes you can use with
|
||||
* keyboard-related events, including `KEY_BACKSPACE`, `KEY_TAB`,
|
||||
* `KEY_RETURN`, `KEY_ESC`, `KEY_LEFT`, `KEY_UP`, `KEY_RIGHT`, `KEY_DOWN`,
|
||||
* `KEY_DELETE`, `KEY_HOME`, `KEY_END`, `KEY_PAGEUP`, `KEY_PAGEDOWN` and
|
||||
* `KEY_INSERT`.
|
||||
*
|
||||
* The functions you're most likely to use a lot are [[Event.observe]],
|
||||
* [[Event.element]] and [[Event.stop]]. If your web app uses custom events,
|
||||
* you'll also get a lot of mileage out of [[Event.fire]].
|
||||
**/
|
||||
var Event = {
|
||||
KEY_BACKSPACE: 8,
|
||||
KEY_TAB: 9,
|
||||
|
@ -18,6 +53,10 @@
|
|||
cache: {}
|
||||
};
|
||||
|
||||
var docEl = document.documentElement;
|
||||
var MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED = 'onmouseenter' in docEl
|
||||
&& 'onmouseleave' in docEl;
|
||||
|
||||
var _isButton;
|
||||
if (Prototype.Browser.IE) {
|
||||
// IE doesn't map left/right/middle the same way.
|
||||
|
@ -41,10 +80,44 @@
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Event.isLeftClick(@event) -> Boolean
|
||||
*
|
||||
* Determines whether a button-related mouse event involved the left
|
||||
* mouse button.
|
||||
*
|
||||
* Keep in mind that the "left" mouse button is actually the "primary" mouse
|
||||
* button. When a mouse is in left-handed mode, the browser will report
|
||||
* clicks of the _right_ button as "left-clicks."
|
||||
**/
|
||||
function isLeftClick(event) { return _isButton(event, 0) }
|
||||
|
||||
/**
|
||||
* Event.isMiddleClick(@event) -> Boolean
|
||||
*
|
||||
* Determines whether a button-related mouse event involved the middle
|
||||
* mouse button.
|
||||
**/
|
||||
function isMiddleClick(event) { return _isButton(event, 1) }
|
||||
|
||||
/**
|
||||
* Event.isRightClick(@event) -> Boolean
|
||||
*
|
||||
* Determines whether a button-related mouse event involved the right
|
||||
* mouse button.
|
||||
*
|
||||
* Keep in mind that the "left" mouse button is actually the "secondary"
|
||||
* mouse button. When a mouse is in left-handed mode, the browser will
|
||||
* report clicks of the _left_ button as "left-clicks."
|
||||
**/
|
||||
function isRightClick(event) { return _isButton(event, 2) }
|
||||
|
||||
/** deprecated
|
||||
* Event.element(@event) -> Element
|
||||
*
|
||||
* Returns the DOM element on which the event occurred. This method
|
||||
* is deprecated, use [[Event.findElement findElement]] instead.
|
||||
**/
|
||||
function element(event) {
|
||||
event = Event.extend(event);
|
||||
|
||||
|
@ -69,29 +142,84 @@
|
|||
return Element.extend(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Event.findElement(@event, expression) -> Element
|
||||
*
|
||||
* Returns the first DOM element that matches a given CSS selector —
|
||||
* starting with the element on which the event occurred, then moving up
|
||||
* its ancestor chain.
|
||||
**/
|
||||
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);
|
||||
while (element) {
|
||||
if (Prototype.Selector.match(element, expression)) {
|
||||
return Element.extend(element);
|
||||
}
|
||||
element = element.parentNode;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Event.pointer(@event) -> Object
|
||||
*
|
||||
* Returns the absolute position of the pointer for a mouse event.
|
||||
*
|
||||
* Returns an object in the form `{ x: Number, y: Number}`.
|
||||
*
|
||||
* Note that this position is absolute on the _page_, not on the
|
||||
* _viewport_.
|
||||
**/
|
||||
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))
|
||||
};
|
||||
return { x: pointerX(event), y: pointerY(event) };
|
||||
}
|
||||
|
||||
function pointerX(event) { return Event.pointer(event).x }
|
||||
function pointerY(event) { return Event.pointer(event).y }
|
||||
/**
|
||||
* Event.pointerX(@event) -> Number
|
||||
*
|
||||
* Returns the absolute horizontal position of the pointer for a mouse
|
||||
* event.
|
||||
*
|
||||
* Note that this position is absolute on the _page_, not on the
|
||||
* _viewport_.
|
||||
**/
|
||||
function pointerX(event) {
|
||||
var docElement = document.documentElement,
|
||||
body = document.body || { scrollLeft: 0 };
|
||||
|
||||
return event.pageX || (event.clientX +
|
||||
(docElement.scrollLeft || body.scrollLeft) -
|
||||
(docElement.clientLeft || 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Event.pointerY(@event) -> Number
|
||||
*
|
||||
* Returns the absolute vertical position of the pointer for a mouse
|
||||
* event.
|
||||
*
|
||||
* Note that this position is absolute on the _page_, not on the
|
||||
* _viewport_.
|
||||
**/
|
||||
function pointerY(event) {
|
||||
var docElement = document.documentElement,
|
||||
body = document.body || { scrollTop: 0 };
|
||||
|
||||
return event.pageY || (event.clientY +
|
||||
(docElement.scrollTop || body.scrollTop) -
|
||||
(docElement.clientTop || 0));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Event.stop(@event) -> undefined
|
||||
*
|
||||
* Stops the event's propagation and prevents its eventual default action
|
||||
* from being triggered.
|
||||
*
|
||||
* Stopping an event also sets a `stopped` property on that event for
|
||||
* future inspection.
|
||||
**/
|
||||
function stop(event) {
|
||||
Event.extend(event);
|
||||
event.preventDefault();
|
||||
|
@ -142,18 +270,22 @@
|
|||
});
|
||||
|
||||
// IE's method for extending events.
|
||||
Event.extend = function(event) {
|
||||
Event.extend = function(event, element) {
|
||||
if (!event) return false;
|
||||
if (event._extendedByPrototype) return event;
|
||||
|
||||
event._extendedByPrototype = Prototype.emptyFunction;
|
||||
var pointer = Event.pointer(event);
|
||||
|
||||
// The optional `element` argument gives us a fallback value for the
|
||||
// `target` property in case IE doesn't give us through `srcElement`.
|
||||
Object.extend(event, {
|
||||
target: event.srcElement,
|
||||
target: event.srcElement || element,
|
||||
relatedTarget: _relatedTarget(event),
|
||||
pageX: pointer.x,
|
||||
pageY: pointer.y
|
||||
});
|
||||
|
||||
return Object.extend(event, methods);
|
||||
};
|
||||
} else {
|
||||
|
@ -162,68 +294,86 @@
|
|||
Event.extend = Prototype.K;
|
||||
}
|
||||
|
||||
function _getEventID(element) {
|
||||
if (element._prototypeEventID) return element._prototypeEventID[0];
|
||||
return element._prototypeEventID = [++arguments.callee.id];
|
||||
}
|
||||
_getEventID.id = 1;
|
||||
|
||||
function _getDOMEventName(eventName) {
|
||||
if (eventName && eventName.include(':')) return 'dataavailable';
|
||||
return eventName;
|
||||
}
|
||||
|
||||
function _getCacheForID(id) {
|
||||
return Event.cache[id] = Event.cache[id] || { };
|
||||
}
|
||||
|
||||
function _getRespondersForEvent(id, eventName) {
|
||||
var c = _getCacheForID(id);
|
||||
return c[eventName] = c[eventName] || [];
|
||||
}
|
||||
|
||||
function _createResponder(element, eventName, handler) {
|
||||
var id = _getEventID(element), r = _getRespondersForEvent(id, eventName);
|
||||
// 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)) {
|
||||
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 (r.pluck('handler').include(handler)) return false;
|
||||
if (respondersForEvent.pluck('handler').include(handler)) return false;
|
||||
|
||||
var responder = function(event) {
|
||||
if (!Event || !Event.extend ||
|
||||
// If it's a custom event, but not the _correct_ custom event, ignore it.
|
||||
(!Object.isUndefined(event.eventName) && event.eventName !== eventName))
|
||||
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;
|
||||
|
||||
Event.extend(event);
|
||||
handler.call(element, event);
|
||||
};
|
||||
// If it's a custom event, but not the _correct_ custom event, ignore it.
|
||||
if (event.eventName !== eventName)
|
||||
return false;
|
||||
|
||||
Event.extend(event, element);
|
||||
handler.call(element, event);
|
||||
};
|
||||
} else {
|
||||
// Non-custom event.
|
||||
if (!MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED &&
|
||||
(eventName === "mouseenter" || eventName === "mouseleave")) {
|
||||
// If we're dealing with mouseenter or mouseleave in a non-IE browser,
|
||||
// we create a custom responder that mimics their behavior within
|
||||
// mouseover and mouseout.
|
||||
if (eventName === "mouseenter" || eventName === "mouseleave") {
|
||||
responder = function(event) {
|
||||
Event.extend(event, element);
|
||||
|
||||
var parent = event.relatedTarget;
|
||||
while (parent && parent !== element) {
|
||||
try { parent = parent.parentNode; }
|
||||
catch(e) { parent = element; }
|
||||
}
|
||||
|
||||
if (parent === element) return;
|
||||
|
||||
handler.call(element, event);
|
||||
};
|
||||
}
|
||||
} else {
|
||||
responder = function(event) {
|
||||
Event.extend(event, element);
|
||||
handler.call(element, event);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
responder.handler = handler;
|
||||
r.push(responder);
|
||||
respondersForEvent.push(responder);
|
||||
return responder;
|
||||
}
|
||||
|
||||
function _findResponder(id, eventName, handler) {
|
||||
var r = _getRespondersForEvent(id, eventName);
|
||||
return r.find(function(responder) {
|
||||
return responder.handler === handler;
|
||||
});
|
||||
}
|
||||
|
||||
function _destroyResponder(id, eventName, handler) {
|
||||
var c = _getCacheForID(id);
|
||||
if (Object.isUndefined(c[eventName])) return false;
|
||||
c[eventName] = c[eventName].without(_findResponder(id, eventName, handler));
|
||||
}
|
||||
|
||||
function _destroyCache() {
|
||||
for (var id in Event.cache) {
|
||||
for (var eventName in Event.cache[id])
|
||||
Event.cache[id][eventName] = null;
|
||||
for (var i = 0, length = CACHE.length; i < length; i++) {
|
||||
Event.stopObserving(CACHE[i]);
|
||||
CACHE[i] = null;
|
||||
}
|
||||
}
|
||||
|
||||
var CACHE = [];
|
||||
|
||||
// Internet Explorer needs to remove event handlers on page unload
|
||||
// in order to avoid memory leaks.
|
||||
if (Prototype.Browser.IE)
|
||||
|
@ -236,56 +386,312 @@
|
|||
window.addEventListener('unload', Prototype.emptyFunction, false);
|
||||
|
||||
|
||||
var _getDOMEventName = Prototype.K,
|
||||
translations = { mouseenter: "mouseover", mouseleave: "mouseout" };
|
||||
|
||||
if (!MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED) {
|
||||
_getDOMEventName = function(eventName) {
|
||||
return (translations[eventName] || eventName);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Event.observe(element, eventName, handler) -> Element
|
||||
* - element (Element | String): The DOM element to observe, or its ID.
|
||||
* - eventName (String): The name of the event, in all lower case, without the "on"
|
||||
* prefix — e.g., "click" (not "onclick").
|
||||
* - handler (Function): The function to call when the event occurs.
|
||||
*
|
||||
* Registers an event handler on a DOM element. Aliased as [[Element#observe]].
|
||||
*
|
||||
* `Event.observe` smooths out a variety of differences between browsers and provides
|
||||
* some handy additional features as well. Key features in brief:
|
||||
* * Several handlers can be registered for the same event on the same element.
|
||||
* * Prototype figures out whether to use `addEventListener` (W3C standard) or
|
||||
* `attachEvent` (MSIE); you don't have to worry about it.
|
||||
* * The handler is passed an _extended_ [[Event]] object (even on MSIE).
|
||||
* * The handler's context (`this` value) is set to the extended element being observed
|
||||
* (even if the event actually occurred on a descendent element and bubbled up).
|
||||
* * Prototype handles cleaning up the handler when leaving the page (important for MSIE memory
|
||||
* leak prevention).
|
||||
* * `observe` makes it possible to stop observing the event easily via [[Event.stopObserving]].
|
||||
* * Adds support for `mouseenter` / `mouseleave` in all browsers.
|
||||
*
|
||||
* Although you can use `Event.observe` directly and there are times when that's the most
|
||||
* convenient or direct way, it's more common to use its alias [[Element#observe]]. These two
|
||||
* statements have the same effect:
|
||||
*
|
||||
* Event.observe('foo', 'click', myHandler);
|
||||
* $('foo').observe('click', myHandler);
|
||||
*
|
||||
* The examples in this documentation use the [[Element#observe]] form.
|
||||
*
|
||||
* <h5>The Handler</h5>
|
||||
*
|
||||
* Signature:
|
||||
*
|
||||
* function handler(event) {
|
||||
* // `this` = the element being observed
|
||||
* }
|
||||
*
|
||||
* So for example, this will turn the background of the element 'foo' blue when it's clicked:
|
||||
*
|
||||
* $('foo').observe('click', function(event) {
|
||||
* this.setStyle({backgroundColor: 'blue'});
|
||||
* });
|
||||
*
|
||||
* Note that we used `this` to refer to the element, and that we received the `event` object
|
||||
* as a parameter (even on MSIE).
|
||||
*
|
||||
* <h5>It's All About Timing</h5>
|
||||
*
|
||||
* One of the most common errors trying to observe events is trying to do it before the element
|
||||
* exists in the DOM. Don't try to observe elements until after the
|
||||
* [[document.observe dom:loaded]] event or `window` `load` event has been fired.
|
||||
*
|
||||
* <h5>Preventing the Default Event Action and Bubbling</h5>
|
||||
*
|
||||
* If we want to stop the event (e.g., prevent its default action and stop it bubbling), we can
|
||||
* do so with the extended event object's [[Event#stop]] method:
|
||||
*
|
||||
* $('foo').observe('click', function(event) {
|
||||
* event.stop();
|
||||
* });
|
||||
*
|
||||
* <h5>Finding the Element Where the Event Occurred</h5>
|
||||
*
|
||||
* Since most events bubble from descendant elements up through the hierarchy until they're
|
||||
* handled, we can observe an event on a container rather than individual elements within the
|
||||
* container. This is sometimes called "event delegation". It's particularly handy for tables:
|
||||
*
|
||||
* <table id='records'>
|
||||
* <thead>
|
||||
* <tr><th colspan='2'>No record clicked</th></tr>
|
||||
* </thead>
|
||||
* <tbody>
|
||||
* <tr data-recnum='1'><td>1</td><td>First record</td></tr>
|
||||
* <tr data-recnum='2'><td>2</td><td>Second record</td></tr>
|
||||
* <tr data-recnum='3'><td>3</td><td>Third record</td></tr>
|
||||
* </tbody>
|
||||
* </table>
|
||||
*
|
||||
* Instead of observing each cell or row, we can simply observe the table:
|
||||
*
|
||||
* $('records').observe('click', function(event) {
|
||||
* var clickedRow;
|
||||
* clickedRow = event.findElement('tr');
|
||||
* if (clickedRow) {
|
||||
* this.down('th').update("You clicked record #" + clickedRow.readAttribute("data-recnum"));
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* When any row in the table is clicked, we update the table's first header cell saying which
|
||||
* record was clicked. [[Event#findElement]] finds the row that was clicked, and `this` refers
|
||||
* to the table we were observing.
|
||||
*
|
||||
* <h5>Stopping Observing the Event</h5>
|
||||
*
|
||||
* If we don't need to observe the event anymore, we can stop observing it with
|
||||
* [[Event.stopObserving]] (aka [[Element#stopObserving]]).
|
||||
*
|
||||
* <h5>Using an Instance Method as a Handler</h5>
|
||||
*
|
||||
* If we want to use an instance method as a handler, we will probably want to use
|
||||
* [[Function#bind]] to set the handler's context; otherwise, the context will be lost and
|
||||
* `this` won't mean what we expect it to mean within the handler function. E.g.:
|
||||
*
|
||||
* var MyClass = Class.create({
|
||||
* initialize: function(name, element) {
|
||||
* this.name = name;
|
||||
* element = $(element);
|
||||
* if (element) {
|
||||
* element.observe(this.handleClick.bind(this));
|
||||
* }
|
||||
* },
|
||||
* handleClick: function(event) {
|
||||
* alert("My name is " + this.name);
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* Without the `bind`, when `handleClick` was triggered by the event, `this` wouldn't
|
||||
* refer to the instance and so the alert wouldn't show the name. Because we used `bind`, it
|
||||
* works correctly. See [[Function#bind]] for
|
||||
* details. There's also [[Function#bindAsEventListener]], which is handy for certain very
|
||||
* specific situations. (Normally, `bind` is all you need.)
|
||||
*
|
||||
* <h5>Side Notes</h5>
|
||||
*
|
||||
* Although Prototype smooths out most of the differences between browsers, the fundamental
|
||||
* behavior of a browser implementation isn't changed. For example, the timing of the `change`
|
||||
* or `blur` events varies a bit from browser to browser.
|
||||
*
|
||||
* <h5>Changes in 1.6.x</h5>
|
||||
*
|
||||
* Prior to Prototype 1.6, `observe` supported a fourth argument (`useCapture`), a boolean that
|
||||
* indicated whether to use the browser's capturing phase or its bubbling phase. Since MSIE does
|
||||
* not support the capturing phase, we removed this argument from 1.6, lest it give users the
|
||||
* false impression that they can use the capturing phase in all browsers.
|
||||
*
|
||||
* 1.6 also introduced setting the `this` context to the element being observed, automatically
|
||||
* extending the [[Event]] object, and the [[Event#findElement]] method.
|
||||
*
|
||||
**/
|
||||
function observe(element, eventName, handler) {
|
||||
element = $(element);
|
||||
var name = _getDOMEventName(eventName),
|
||||
responder = _createResponder(element, eventName, handler);
|
||||
|
||||
var responder = _createResponder(element, eventName, handler);
|
||||
|
||||
if (!responder) return element;
|
||||
|
||||
if (element.addEventListener)
|
||||
element.addEventListener(name, responder, false);
|
||||
else
|
||||
element.attachEvent("on" + name, responder);
|
||||
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 {
|
||||
var actualEventName = _getDOMEventName(eventName);
|
||||
|
||||
// Ordinary event.
|
||||
if (element.addEventListener)
|
||||
element.addEventListener(actualEventName, responder, false);
|
||||
else
|
||||
element.attachEvent("on" + actualEventName, responder);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event.stopObserving(element[, eventName[, handler]]) -> Element
|
||||
* - element (Element | String): The element to stop observing, or its ID.
|
||||
* - eventName (String): _(Optional)_ The name of the event to stop observing, in all lower case,
|
||||
* without the "on" — e.g., "click" (not "onclick").
|
||||
* - handler (Function): _(Optional)_ The handler to remove; must be the _exact same_ reference
|
||||
* that was passed to [[Event.observe]] (see below.).
|
||||
*
|
||||
* Unregisters one or more event handlers.
|
||||
*
|
||||
* If `handler` is omitted, unregisters all event handlers on `element`
|
||||
* for that `eventName`. If `eventName` is also omitted, unregisters _all_
|
||||
* event handlers on `element`. (In each case, only affects handlers registered via Prototype.)
|
||||
*
|
||||
* <h5>Examples</h5>
|
||||
*
|
||||
* Assuming:
|
||||
*
|
||||
* $('foo').observe('click', myHandler);
|
||||
*
|
||||
* ...we can stop observing using that handler like so:
|
||||
*
|
||||
* $('foo').stopObserving('click', myHandler);
|
||||
*
|
||||
* If we want to remove _all_ 'click' handlers from 'foo', we leave off the handler argument:
|
||||
*
|
||||
* $('foo').stopObserving('click');
|
||||
*
|
||||
* If we want to remove _all_ handlers for _all_ events from 'foo' (perhaps we're about to remove
|
||||
* it from the DOM), we simply omit both the handler and the event name:
|
||||
*
|
||||
* $('foo').stopObserving();
|
||||
*
|
||||
* <h5>A Common Error</h5>
|
||||
*
|
||||
* When using instance methods as observers, it's common to use [[Function#bind]] on them, e.g.:
|
||||
*
|
||||
* $('foo').observe('click', this.handlerMethod.bind(this));
|
||||
*
|
||||
* If you do that, __this will not work__ to unregister the handler:
|
||||
*
|
||||
* $('foo').stopObserving('click', this.handlerMethod.bind(this)); // <== WRONG
|
||||
*
|
||||
* [[Function#bind]] returns a _new_ function every time it's called, and so if you don't retain
|
||||
* the reference you used when observing, you can't unhook that function specifically. (You can
|
||||
* still unhook __all__ handlers for an event, or all handlers on the element entirely.)
|
||||
*
|
||||
* To do this, you need to keep a reference to the bound function:
|
||||
*
|
||||
* this.boundHandlerMethod = this.handlerMethod.bind(this);
|
||||
* $('foo').observe('click', this.boundHandlerMethod);
|
||||
*
|
||||
* ...and then to remove:
|
||||
*
|
||||
* $('foo').stopObserving('click', this.boundHandlerMethod); // <== Right
|
||||
*
|
||||
**/
|
||||
function stopObserving(element, eventName, handler) {
|
||||
element = $(element);
|
||||
var id = _getEventID(element), name = _getDOMEventName(eventName);
|
||||
|
||||
if (eventName && !handler) {
|
||||
// If an event name is passed without a handler, we stop observing all
|
||||
// handlers of that type.
|
||||
_getRespondersForEvent(id, eventName).each(function(r) {
|
||||
element.stopObserving(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.
|
||||
Object.keys(_getCacheForID(id)).each(function(eventName) {
|
||||
element.stopObserving(eventName);
|
||||
var registry = Element.retrieve(element, 'prototype_event_registry');
|
||||
if (!registry) return element;
|
||||
|
||||
if (!eventName) {
|
||||
// We stop observing all events.
|
||||
// e.g.: $(element).stopObserving();
|
||||
registry.each( function(pair) {
|
||||
var eventName = pair.key;
|
||||
stopObserving(element, eventName);
|
||||
});
|
||||
return element;
|
||||
}
|
||||
|
||||
var responder = _findResponder(id, eventName, handler);
|
||||
var responders = registry.get(eventName);
|
||||
if (!responders) return element;
|
||||
|
||||
if (!handler) {
|
||||
// We stop observing all handlers for the given eventName.
|
||||
// e.g.: $(element).stopObserving('click');
|
||||
responders.each(function(r) {
|
||||
stopObserving(element, eventName, r.handler);
|
||||
});
|
||||
return element;
|
||||
}
|
||||
|
||||
var responder = responders.find( function(r) { return r.handler === handler; });
|
||||
if (!responder) return element;
|
||||
|
||||
if (element.removeEventListener)
|
||||
element.removeEventListener(name, responder, false);
|
||||
else
|
||||
element.detachEvent('on' + name, responder);
|
||||
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.
|
||||
var actualEventName = _getDOMEventName(eventName);
|
||||
if (element.removeEventListener)
|
||||
element.removeEventListener(actualEventName, responder, false);
|
||||
else
|
||||
element.detachEvent('on' + actualEventName, responder);
|
||||
}
|
||||
|
||||
_destroyResponder(id, eventName, handler);
|
||||
registry.set(eventName, responders.without(responder));
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
function fire(element, eventName, memo) {
|
||||
/**
|
||||
* Event.fire(element, eventName[, memo[, bubble = true]]) -> Event
|
||||
* - memo (?): Metadata for the event. Will be accessible through the
|
||||
* event's `memo` property.
|
||||
* - bubble (Boolean): Whether the event will bubble.
|
||||
*
|
||||
* Fires a custom event of name `eventName` with `element` as its target.
|
||||
*
|
||||
* Custom events must include a colon (`:`) in their names.
|
||||
**/
|
||||
function fire(element, eventName, memo, bubble) {
|
||||
element = $(element);
|
||||
|
||||
if (Object.isUndefined(bubble))
|
||||
bubble = true;
|
||||
|
||||
if (element == document && document.createEvent && !element.dispatchEvent)
|
||||
element = document.documentElement;
|
||||
|
||||
|
@ -295,7 +701,7 @@
|
|||
event.initEvent('dataavailable', true, true);
|
||||
} else {
|
||||
event = document.createEventObject();
|
||||
event.eventType = 'ondataavailable';
|
||||
event.eventType = bubble ? 'ondataavailable' : 'onfilterchange';
|
||||
}
|
||||
|
||||
event.eventName = eventName;
|
||||
|
@ -319,60 +725,100 @@
|
|||
});
|
||||
|
||||
Element.addMethods({
|
||||
/**
|
||||
* Element.fire(@element, eventName[, memo[, bubble = true]]) -> Event
|
||||
* See [[Event.fire]].
|
||||
**/
|
||||
fire: fire,
|
||||
|
||||
/**
|
||||
* Element.observe(@element, eventName, handler) -> Element
|
||||
* See [[Event.observe]].
|
||||
**/
|
||||
observe: observe,
|
||||
|
||||
/**
|
||||
* Element.stopObserving(@element[, eventName[, handler]]) -> Element
|
||||
* See [[Event.stopObserving]].
|
||||
**/
|
||||
stopObserving: stopObserving
|
||||
});
|
||||
|
||||
/** section: DOM
|
||||
* document
|
||||
*
|
||||
* Prototype extends the built-in `document` object with several convenience
|
||||
* methods related to events.
|
||||
**/
|
||||
Object.extend(document, {
|
||||
/**
|
||||
* document.fire(eventName[, memo[, bubble = true]]) -> Event
|
||||
* See [[Event.fire]].
|
||||
**/
|
||||
fire: fire.methodize(),
|
||||
|
||||
/**
|
||||
* document.observe(eventName, handler) -> Element
|
||||
* See [[Event.observe]].
|
||||
**/
|
||||
observe: observe.methodize(),
|
||||
|
||||
/**
|
||||
* document.stopObserving([eventName[, handler]]) -> Element
|
||||
* See [[Event.stopObserving]].
|
||||
**/
|
||||
stopObserving: stopObserving.methodize(),
|
||||
|
||||
/**
|
||||
* document.loaded -> Boolean
|
||||
*
|
||||
* Whether the full DOM tree is ready for manipulation.
|
||||
**/
|
||||
loaded: false
|
||||
});
|
||||
|
||||
// Export to the global scope.
|
||||
if (window.Event) Object.extend(window.Event, Event)
|
||||
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 and John Resig. */
|
||||
/* 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() {
|
||||
var timer;
|
||||
|
||||
function fireContentLoadedEvent() {
|
||||
if (document.loaded) return;
|
||||
if (_timer) window.clearInterval(_timer);
|
||||
|
||||
document.fire("dom:loaded");
|
||||
if (timer) window.clearTimeout(timer);
|
||||
document.loaded = true;
|
||||
document.fire('dom:loaded');
|
||||
}
|
||||
|
||||
function _webkitContentLoadedCheck() {
|
||||
var s = document.readyState;
|
||||
if (s === "loaded" || s === "complete")
|
||||
_fireContentLoadedEvent();
|
||||
}
|
||||
|
||||
function _IEContentLoadedCheck() {
|
||||
if (this.readyState == "complete") {
|
||||
this.onreadystatechange = null;
|
||||
_fireContentLoadedEvent();
|
||||
|
||||
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) {
|
||||
if (Prototype.Browser.WebKit) {
|
||||
_timer = window.setInterval(_webkitContentLoadedCheck, 0);
|
||||
Event.observe(window, "load", _fireContentLoadedEvent);
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded",
|
||||
_fireContentLoadedEvent, false);
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', fireContentLoadedEvent, false);
|
||||
} else {
|
||||
document.write("<script id=__onDOMContentLoaded defer src=//:><\/script>");
|
||||
$("__onDOMContentLoaded").onreadystatechange = _IEContentLoadedCheck;
|
||||
document.observe('readystatechange', checkReadyState);
|
||||
if (window == top)
|
||||
timer = pollDoScroll.defer();
|
||||
}
|
||||
})();
|
||||
|
||||
// Worst-case fallback
|
||||
Event.observe(window, 'load', fireContentLoadedEvent);
|
||||
})();
|
||||
|
|
381
src/dom/form.js
381
src/dom/form.js
|
@ -1,19 +1,89 @@
|
|||
/** section: DOM
|
||||
* Form
|
||||
*
|
||||
* Utilities for dealing with forms in the DOM.
|
||||
*
|
||||
* `Form` is a namespace for all things form-related, packed with form
|
||||
* manipulation and serialization goodness. While it holds methods dealing
|
||||
* with forms as a whole, its submodule [[Form.Element]] deals with specific
|
||||
* form controls.
|
||||
*
|
||||
* Many of these methods are also available directly on `form` elements.
|
||||
**/
|
||||
|
||||
var Form = {
|
||||
/**
|
||||
* Form.reset(form) -> Element
|
||||
*
|
||||
* Resets a form to its default values.
|
||||
**/
|
||||
reset: function(form) {
|
||||
$(form).reset();
|
||||
form = $(form);
|
||||
form.reset();
|
||||
return form;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Form.serializeElements(elements[, options]) -> String | Object
|
||||
* - elements (Array): A collection of elements to include in the
|
||||
* serialization.
|
||||
* - options (Object): _(Optional)_ A set of options that affect the return
|
||||
* value of the method.
|
||||
*
|
||||
* Serialize an array of form elements to an object or string suitable
|
||||
* for [[Ajax]] requests.
|
||||
*
|
||||
* As per the HTML spec, disabled fields are not included.
|
||||
*
|
||||
* If multiple elements have the same name and we're returning an object,
|
||||
* the value for that key in the object will be an array of the field values
|
||||
* in the order they appeared on the array of elements.
|
||||
*
|
||||
* <h5>The Options</h5>
|
||||
*
|
||||
* The options allow you to control two things: What kind of return
|
||||
* value you get (an object or a string), and whether and which `submit`
|
||||
* fields are included in that object or string.
|
||||
*
|
||||
* If you do not supply an `options` object _at all_, the options
|
||||
* `{ hash: false }` are used.
|
||||
*
|
||||
* If you supply an `options` object, it may have the following options:
|
||||
* - `hash` ([[Boolean]]): `true` to return a plain object with keys and values
|
||||
* (not a [[Hash]]; see below), `false` to return a String in query string
|
||||
* format. If you supply an `options` object with no `hash` member, `hash`
|
||||
* defaults to `true`. Note that this is __not__ the same as leaving off the
|
||||
* `options` object entirely (see above).
|
||||
* - `submit` ([[Boolean]] | [[String]]): In essence: If you omit this option the
|
||||
* first submit button in the form is included; if you supply `false`,
|
||||
* no submit buttons are included; if you supply the name of a submit
|
||||
* button, the first button with that name is included. Note that the `false`
|
||||
* value __must__ really be `false`, not _falsey_; falsey-but-not-false is
|
||||
* like omitting the option.
|
||||
*
|
||||
* _(Deprecated)_ If you pass in a [[Boolean]] instead of an object for `options`, it
|
||||
* is used as the `hash` option and all other options are defaulted.
|
||||
*
|
||||
* <h5>A <em>hash</em>, not a Hash</h5>
|
||||
*
|
||||
* If you opt to receive an object, it is a plain JavaScript object with keys
|
||||
* and values, __not__ a [[Hash]]. All JavaScript objects are hashes in the lower-case
|
||||
* sense of the word, which is why the option has that somewhat-confusing name.
|
||||
**/
|
||||
serializeElements: function(elements, options) {
|
||||
// An earlier version accepted a boolean second parameter (hash) where
|
||||
// the default if omitted was false; respect that, but if they pass in an
|
||||
// options object (e.g., the new signature) but don't specify the hash option,
|
||||
// default true, as that's the new preferred approach.
|
||||
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)))) {
|
||||
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]];
|
||||
|
@ -24,32 +94,65 @@ var Form = {
|
|||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
|
||||
return options.hash ? data : Object.toQueryString(data);
|
||||
}
|
||||
};
|
||||
|
||||
Form.Methods = {
|
||||
/**
|
||||
* Form.serialize(@form[, options]) -> String | Object
|
||||
* - options (Object): A list of options that affect the return value
|
||||
* of the method.
|
||||
*
|
||||
* Serialize form data to an object or string suitable for Ajax requests.
|
||||
*
|
||||
* See [[Form.serializeElements]] for details on the options.
|
||||
**/
|
||||
serialize: function(form, options) {
|
||||
return Form.serializeElements(Form.getElements(form), options);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Form.getElements(@form) -> [Element...]
|
||||
*
|
||||
* Returns a collection of all controls within a form.
|
||||
**/
|
||||
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;
|
||||
}
|
||||
);
|
||||
var elements = $(form).getElementsByTagName('*'),
|
||||
element,
|
||||
arr = [ ],
|
||||
serializers = Form.Element.Serializers;
|
||||
// `length` is not used to prevent interference with
|
||||
// length-named elements shadowing `length` of a nodelist
|
||||
for (var i = 0; element = elements[i]; i++) {
|
||||
arr.push(element);
|
||||
}
|
||||
return arr.inject([], function(elements, child) {
|
||||
if (serializers[child.tagName.toLowerCase()])
|
||||
elements.push(Element.extend(child));
|
||||
return elements;
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Form.getInputs(@form [, type [, name]]) -> [Element...]
|
||||
* - type (String): A value for the `type` attribute against which to
|
||||
* filter.
|
||||
* - name (String): A value for the `name` attribute against which to
|
||||
* filter.
|
||||
*
|
||||
* Returns a collection of all `INPUT` elements in a form.
|
||||
*
|
||||
* Use optional `type` and `name` arguments to restrict the search on
|
||||
* these attributes.
|
||||
**/
|
||||
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))
|
||||
|
@ -60,18 +163,34 @@ Form.Methods = {
|
|||
return matchingInputs;
|
||||
},
|
||||
|
||||
/**
|
||||
* Form.disable(@form) -> Element
|
||||
*
|
||||
* Disables the form as a whole. Form controls will be visible but
|
||||
* uneditable.
|
||||
**/
|
||||
disable: function(form) {
|
||||
form = $(form);
|
||||
Form.getElements(form).invoke('disable');
|
||||
return form;
|
||||
},
|
||||
|
||||
/**
|
||||
* Form.enable(@form) -> Element
|
||||
*
|
||||
* Enables a fully- or partially-disabled form.
|
||||
**/
|
||||
enable: function(form) {
|
||||
form = $(form);
|
||||
Form.getElements(form).invoke('enable');
|
||||
return form;
|
||||
},
|
||||
|
||||
/**
|
||||
* Form.findFirstElement(@form) -> Element
|
||||
*
|
||||
* Finds the first non-hidden, non-disabled control within the form.
|
||||
**/
|
||||
findFirstElement: function(form) {
|
||||
var elements = $(form).getElements().findAll(function(element) {
|
||||
return 'hidden' != element.type && !element.disabled;
|
||||
|
@ -79,45 +198,92 @@ Form.Methods = {
|
|||
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());
|
||||
return /^(?:input|select|textarea)$/i.test(element.tagName);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Form.focusFirstElement(@form) -> Element
|
||||
*
|
||||
* Gives keyboard focus to the first element of the form. Returns the form.
|
||||
**/
|
||||
focusFirstElement: function(form) {
|
||||
form = $(form);
|
||||
form.findFirstElement().activate();
|
||||
return form;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Form.request(@form[, options]) -> Ajax.Request
|
||||
* - options (Object): Options to pass along to the `Ajax.Request`
|
||||
* constructor.
|
||||
*
|
||||
* A convenience method for serializing and submitting the form via an
|
||||
* [[Ajax.Request]] to the URL of the form's `action` attribute.
|
||||
*
|
||||
* The `options` parameter is passed to the `Ajax.Request` instance,
|
||||
* allowing one to override the HTTP method and/or specify additional
|
||||
* parameters and callbacks.
|
||||
**/
|
||||
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
|
||||
*
|
||||
* Utilities for dealing with form controls in the DOM.
|
||||
*
|
||||
* This is a collection of methods that assist in dealing with form controls.
|
||||
* They provide ways to focus, serialize, disable/enable or extract current
|
||||
* value from a specific control.
|
||||
*
|
||||
* Note that nearly all these methods are available directly on `input`,
|
||||
* `select`, and `textarea` elements. Therefore, these are equivalent:
|
||||
*
|
||||
* Form.Element.activate('myfield');
|
||||
* $('myfield').activate();
|
||||
*
|
||||
* Naturally, you should always prefer the shortest form suitable in a
|
||||
* situation. Most of these methods also return the element itself (as
|
||||
* indicated by the return type) for chainability.
|
||||
**/
|
||||
|
||||
Form.Element = {
|
||||
/**
|
||||
* Form.Element.focus(element) -> Element
|
||||
*
|
||||
* Gives keyboard focus to an element. Returns the element.
|
||||
**/
|
||||
focus: function(element) {
|
||||
$(element).focus();
|
||||
return element;
|
||||
},
|
||||
|
||||
/**
|
||||
* Form.Element.select(element) -> Element
|
||||
*
|
||||
* Selects the current text in a text input. Returns the element.
|
||||
**/
|
||||
select: function(element) {
|
||||
$(element).select();
|
||||
return element;
|
||||
|
@ -125,6 +291,13 @@ Form.Element = {
|
|||
};
|
||||
|
||||
Form.Element.Methods = {
|
||||
|
||||
/**
|
||||
* Form.Element.serialize(@element) -> String
|
||||
*
|
||||
* Returns a URL-encoded string representation of a form control in the
|
||||
* `name=value` format.
|
||||
**/
|
||||
serialize: function(element) {
|
||||
element = $(element);
|
||||
if (!element.disabled && element.name) {
|
||||
|
@ -137,13 +310,28 @@ Form.Element.Methods = {
|
|||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
|
||||
/** alias of: $F
|
||||
* Form.Element.getValue(@element) -> String | Array
|
||||
*
|
||||
* Returns the current value of a form control.
|
||||
*
|
||||
* A string is returned for most controls; only multiple `select` boxes
|
||||
* return an array of values.
|
||||
*
|
||||
* The global shortcut for this method is [[$F]].
|
||||
**/
|
||||
getValue: function(element) {
|
||||
element = $(element);
|
||||
var method = element.tagName.toLowerCase();
|
||||
return Form.Element.Serializers[method](element);
|
||||
},
|
||||
|
||||
/**
|
||||
* Form.Element.setValue(@element, value) -> Element
|
||||
*
|
||||
* Sets `value` to be the value of the form control. Returns the element.
|
||||
**/
|
||||
setValue: function(element, value) {
|
||||
element = $(element);
|
||||
var method = element.tagName.toLowerCase();
|
||||
|
@ -151,32 +339,59 @@ Form.Element.Methods = {
|
|||
return element;
|
||||
},
|
||||
|
||||
/**
|
||||
* Form.Element.clear(@element) -> Element
|
||||
*
|
||||
* Clears the contents of a text input. Returns the element.
|
||||
**/
|
||||
clear: function(element) {
|
||||
$(element).value = '';
|
||||
return element;
|
||||
},
|
||||
|
||||
/**
|
||||
* Form.Element.present(@element) -> Element
|
||||
*
|
||||
* Returns `true` if a text input has contents, `false` otherwise.
|
||||
**/
|
||||
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.
|
||||
**/
|
||||
activate: function(element) {
|
||||
element = $(element);
|
||||
try {
|
||||
element.focus();
|
||||
if (element.select && (element.tagName.toLowerCase() != 'input' ||
|
||||
!['button', 'reset', 'submit'].include(element.type)))
|
||||
!(/^(?: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.
|
||||
**/
|
||||
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;
|
||||
|
@ -187,6 +402,10 @@ Form.Element.Methods = {
|
|||
/*--------------------------------------------------------------------------*/
|
||||
|
||||
var Field = Form.Element;
|
||||
|
||||
/** section: DOM, related to: Form
|
||||
* $F(element) -> String | Array
|
||||
**/
|
||||
var $F = Form.Element.Methods.getValue;
|
||||
|
||||
/*--------------------------------------------------------------------------*/
|
||||
|
@ -194,7 +413,7 @@ var $F = Form.Element.Methods.getValue;
|
|||
Form.Element.Serializers = {
|
||||
input: function(element, value) {
|
||||
switch (element.type.toLowerCase()) {
|
||||
case 'checkbox':
|
||||
case 'checkbox':
|
||||
case 'radio':
|
||||
return Form.Element.Serializers.inputSelector(element, value);
|
||||
default:
|
||||
|
@ -211,10 +430,10 @@ Form.Element.Serializers = {
|
|||
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' ?
|
||||
return this[element.type == 'select-one' ?
|
||||
'selectOne' : 'selectMany'](element);
|
||||
else {
|
||||
var opt, currentValue, single = !Object.isArray(value);
|
||||
|
@ -231,23 +450,23 @@ Form.Element.Serializers = {
|
|||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
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;
|
||||
|
@ -256,13 +475,49 @@ Form.Element.Serializers = {
|
|||
|
||||
/*--------------------------------------------------------------------------*/
|
||||
|
||||
/** 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.
|
||||
*
|
||||
* <h5>Creating Your Own TimedObserver Implementations</h5>
|
||||
*
|
||||
* 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) ?
|
||||
|
@ -273,13 +528,48 @@ Abstract.TimedObserver = Class.create(PeriodicalExecuter, {
|
|||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 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 = 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]]).
|
||||
**/
|
||||
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);
|
||||
}
|
||||
|
@ -287,18 +577,21 @@ Form.Observer = Class.create(Abstract.TimedObserver, {
|
|||
|
||||
/*--------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
|
@ -306,15 +599,15 @@ Abstract.EventObserver = Class.create({
|
|||
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 'checkbox':
|
||||
case 'radio':
|
||||
Event.observe(element, 'click', this.onElementEvent.bind(this));
|
||||
break;
|
||||
|
@ -322,16 +615,22 @@ Abstract.EventObserver = Class.create({
|
|||
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);
|
||||
|
|
|
@ -1,746 +1,66 @@
|
|||
/* Portions of the Selector 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. */
|
||||
/** section: DOM, related to: Prototype.Selector
|
||||
* $$(expression...) -> [Element...]
|
||||
*
|
||||
* Returns all elements in the document that match the provided CSS selectors.
|
||||
**/
|
||||
|
||||
var Selector = 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;
|
||||
window.$$ = function() {
|
||||
var expression = $A(arguments).join(', ');
|
||||
return Prototype.Selector.select(expression, document);
|
||||
};
|
||||
|
||||
// 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;
|
||||
/**
|
||||
* Prototype.Selector
|
||||
*
|
||||
* A namespace that acts as a wrapper around
|
||||
* the choosen selector engine (Sizzle by default).
|
||||
*
|
||||
**/
|
||||
|
||||
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');
|
||||
// Implementation provided by selector engine.
|
||||
|
||||
// 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);
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
compileMatcher: function() {
|
||||
var e = this.expression, ps = Selector.patterns, h = Selector.handlers,
|
||||
c = Selector.criteria, le, p, m;
|
||||
/**
|
||||
* 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.
|
||||
**/
|
||||
|
||||
if (Selector._cache[e]) {
|
||||
this.matcher = Selector._cache[e];
|
||||
return;
|
||||
}
|
||||
|
||||
this.matcher = ["this.matcher = function(root) {",
|
||||
"var r = root, h = Selector.handlers, c = false, n;"];
|
||||
// Implementation provided by selector engine.
|
||||
|
||||
while (e && le != e && (/\S/).test(e)) {
|
||||
le = e;
|
||||
for (var i in ps) {
|
||||
p = ps[i];
|
||||
if (m = e.match(p)) {
|
||||
this.matcher.push(Object.isFunction(c[i]) ? c[i](m) :
|
||||
new Template(c[i]).evaluate(m));
|
||||
e = e.replace(m[0], '');
|
||||
break;
|
||||
}
|
||||
/**
|
||||
* Prototype.Selector.match(element, expression) -> Boolean
|
||||
* - element (Element): a DOM element.
|
||||
* - expression (String): A CSS selector.
|
||||
*
|
||||
* Tests whether `element` matches the CSS selector.
|
||||
**/
|
||||
|
||||
// Implementation provided by selector engine.
|
||||
|
||||
/**
|
||||
* Prototype.Selector.find(elements, expression[, index = 0]) -> Element
|
||||
* - elements (Enumerable): a collection of DOM elements.
|
||||
* - expression (String): A CSS selector.
|
||||
* - index: 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).
|
||||
**/
|
||||
if (!Prototype.Selector.find) {
|
||||
Prototype.Selector.find = function(elements, expression, index) {
|
||||
if (Object.isUndefined(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 Element.extend(elements[i]);
|
||||
}
|
||||
}
|
||||
|
||||
this.matcher.push("return h.unique(n);\n}");
|
||||
eval(this.matcher.join('\n'));
|
||||
Selector._cache[this.expression] = this.matcher;
|
||||
},
|
||||
|
||||
compileXPathMatcher: function() {
|
||||
var e = this.expression, ps = Selector.patterns,
|
||||
x = Selector.xpath, le, m;
|
||||
|
||||
if (Selector._cache[e]) {
|
||||
this.xpath = Selector._cache[e]; return;
|
||||
}
|
||||
|
||||
this.matcher = ['.//*'];
|
||||
while (e && le != e && (/\S/).test(e)) {
|
||||
le = e;
|
||||
for (var i in ps) {
|
||||
if (m = e.match(ps[i])) {
|
||||
this.matcher.push(Object.isFunction(x[i]) ? x[i](m) :
|
||||
new Template(x[i]).evaluate(m));
|
||||
e = e.replace(m[0], '');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.xpath = this.matcher.join('');
|
||||
Selector._cache[this.expression] = this.xpath;
|
||||
},
|
||||
|
||||
findElements: function(root) {
|
||||
root = root || document;
|
||||
var e = this.expression, results;
|
||||
|
||||
switch (this.mode) {
|
||||
case 'selectorsAPI':
|
||||
// querySelectorAll queries document-wide, then filters to descendants
|
||||
// of the context element. That's not what we want.
|
||||
// Add an explicit context to the selector if necessary.
|
||||
if (root !== document) {
|
||||
var oldId = root.id, id = $(root).identify();
|
||||
e = "#" + id + " " + e;
|
||||
}
|
||||
|
||||
results = $A(root.querySelectorAll(e)).map(Element.extend);
|
||||
root.id = oldId;
|
||||
|
||||
return results;
|
||||
case 'xpath':
|
||||
return document._getElementsByXPath(this.xpath, root);
|
||||
default:
|
||||
return this.matcher(root);
|
||||
}
|
||||
},
|
||||
|
||||
match: function(element) {
|
||||
this.tokens = [];
|
||||
|
||||
var e = this.expression, ps = Selector.patterns, as = Selector.assertions;
|
||||
var le, p, m;
|
||||
|
||||
while (e && le !== e && (/\S/).test(e)) {
|
||||
le = e;
|
||||
for (var i in ps) {
|
||||
p = ps[i];
|
||||
if (m = e.match(p)) {
|
||||
// use the Selector.assertions methods unless the selector
|
||||
// is too complex.
|
||||
if (as[i]) {
|
||||
this.tokens.push([i, Object.clone(m)]);
|
||||
e = e.replace(m[0], '');
|
||||
} else {
|
||||
// reluctantly do a document-wide search
|
||||
// and look for a match in the array
|
||||
return this.findElements(document).include(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var match = true, name, matches;
|
||||
for (var i = 0, token; token = this.tokens[i]; i++) {
|
||||
name = token[0], matches = token[1];
|
||||
if (!Selector.assertions[name](element, matches)) {
|
||||
match = false; break;
|
||||
}
|
||||
}
|
||||
|
||||
return match;
|
||||
},
|
||||
|
||||
toString: function() {
|
||||
return this.expression;
|
||||
},
|
||||
|
||||
inspect: function() {
|
||||
return "#<Selector:" + this.expression.inspect() + ">";
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
alert(isIgnored);
|
||||
return isIgnored;
|
||||
})();
|
||||
}
|
||||
|
||||
Object.extend(Selector, {
|
||||
_cache: { },
|
||||
|
||||
xpath: {
|
||||
descendant: "//*",
|
||||
child: "/*",
|
||||
adjacent: "/following-sibling::*[1]",
|
||||
laterSibling: '/following-sibling::*',
|
||||
tagName: function(m) {
|
||||
if (m[1] == '*') return '';
|
||||
return "[local-name()='" + m[1].toLowerCase() +
|
||||
"' or local-name()='" + m[1].toUpperCase() + "']";
|
||||
},
|
||||
className: "[contains(concat(' ', @class, ' '), ' #{1} ')]",
|
||||
id: "[@id='#{1}']",
|
||||
attrPresence: function(m) {
|
||||
m[1] = m[1].toLowerCase();
|
||||
return new Template("[@#{1}]").evaluate(m);
|
||||
},
|
||||
attr: function(m) {
|
||||
m[1] = m[1].toLowerCase();
|
||||
m[3] = m[5] || m[6];
|
||||
return new Template(Selector.xpath.operators[m[2]]).evaluate(m);
|
||||
},
|
||||
pseudo: function(m) {
|
||||
var h = Selector.xpath.pseudos[m[1]];
|
||||
if (!h) return '';
|
||||
if (Object.isFunction(h)) return h(m);
|
||||
return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m);
|
||||
},
|
||||
operators: {
|
||||
'=': "[@#{1}='#{3}']",
|
||||
'!=': "[@#{1}!='#{3}']",
|
||||
'^=': "[starts-with(@#{1}, '#{3}')]",
|
||||
'$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']",
|
||||
'*=': "[contains(@#{1}, '#{3}')]",
|
||||
'~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]",
|
||||
'|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]"
|
||||
},
|
||||
pseudos: {
|
||||
'first-child': '[not(preceding-sibling::*)]',
|
||||
'last-child': '[not(following-sibling::*)]',
|
||||
'only-child': '[not(preceding-sibling::* or following-sibling::*)]',
|
||||
'empty': "[count(*) = 0 and (count(text()) = 0)]",
|
||||
'checked': "[@checked]",
|
||||
'disabled': "[(@disabled) and (@type!='hidden')]",
|
||||
'enabled': "[not(@disabled) and (@type!='hidden')]",
|
||||
'not': function(m) {
|
||||
var e = m[6], p = Selector.patterns,
|
||||
x = Selector.xpath, le, v;
|
||||
|
||||
var exclusion = [];
|
||||
while (e && le != e && (/\S/).test(e)) {
|
||||
le = e;
|
||||
for (var i in p) {
|
||||
if (m = e.match(p[i])) {
|
||||
v = Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).evaluate(m);
|
||||
exclusion.push("(" + v.substring(1, v.length - 1) + ")");
|
||||
e = e.replace(m[0], '');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "[not(" + exclusion.join(" and ") + ")]";
|
||||
},
|
||||
'nth-child': function(m) {
|
||||
return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m);
|
||||
},
|
||||
'nth-last-child': function(m) {
|
||||
return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m);
|
||||
},
|
||||
'nth-of-type': function(m) {
|
||||
return Selector.xpath.pseudos.nth("position() ", m);
|
||||
},
|
||||
'nth-last-of-type': function(m) {
|
||||
return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m);
|
||||
},
|
||||
'first-of-type': function(m) {
|
||||
m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m);
|
||||
},
|
||||
'last-of-type': function(m) {
|
||||
m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m);
|
||||
},
|
||||
'only-of-type': function(m) {
|
||||
var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m);
|
||||
},
|
||||
nth: function(fragment, m) {
|
||||
var mm, formula = m[6], predicate;
|
||||
if (formula == 'even') formula = '2n+0';
|
||||
if (formula == 'odd') formula = '2n+1';
|
||||
if (mm = formula.match(/^(\d+)$/)) // digit only
|
||||
return '[' + fragment + "= " + mm[1] + ']';
|
||||
if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
|
||||
if (mm[1] == "-") mm[1] = -1;
|
||||
var a = mm[1] ? Number(mm[1]) : 1;
|
||||
var b = mm[2] ? Number(mm[2]) : 0;
|
||||
predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " +
|
||||
"((#{fragment} - #{b}) div #{a} >= 0)]";
|
||||
return new Template(predicate).evaluate({
|
||||
fragment: fragment, a: a, b: b });
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
criteria: {
|
||||
tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;',
|
||||
className: 'n = h.className(n, r, "#{1}", c); c = false;',
|
||||
id: 'n = h.id(n, r, "#{1}", c); c = false;',
|
||||
attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;',
|
||||
attr: function(m) {
|
||||
m[3] = (m[5] || m[6]);
|
||||
return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m);
|
||||
},
|
||||
pseudo: function(m) {
|
||||
if (m[6]) m[6] = m[6].replace(/"/g, '\\"');
|
||||
return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m);
|
||||
},
|
||||
descendant: 'c = "descendant";',
|
||||
child: 'c = "child";',
|
||||
adjacent: 'c = "adjacent";',
|
||||
laterSibling: 'c = "laterSibling";'
|
||||
},
|
||||
|
||||
patterns: {
|
||||
// combinators must be listed first
|
||||
// (and descendant needs to be last combinator)
|
||||
laterSibling: /^\s*~\s*/,
|
||||
child: /^\s*>\s*/,
|
||||
adjacent: /^\s*\+\s*/,
|
||||
descendant: /^\s/,
|
||||
|
||||
// selectors follow
|
||||
tagName: /^\s*(\*|[\w\-]+)(\b|$)?/,
|
||||
id: /^#([\w\-\*]+)(\b|$)/,
|
||||
className: /^\.([\w\-\*]+)(\b|$)/,
|
||||
pseudo:
|
||||
/^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/,
|
||||
attrPresence: /^\[((?:[\w]+:)?[\w]+)\]/,
|
||||
attr: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/
|
||||
},
|
||||
|
||||
// for Selector.match and Element#match
|
||||
assertions: {
|
||||
tagName: function(element, matches) {
|
||||
return matches[1].toUpperCase() == element.tagName.toUpperCase();
|
||||
},
|
||||
|
||||
className: function(element, matches) {
|
||||
return Element.hasClassName(element, matches[1]);
|
||||
},
|
||||
|
||||
id: function(element, matches) {
|
||||
return element.id === matches[1];
|
||||
},
|
||||
|
||||
attrPresence: function(element, matches) {
|
||||
return Element.hasAttribute(element, matches[1]);
|
||||
},
|
||||
|
||||
attr: function(element, matches) {
|
||||
var nodeValue = Element.readAttribute(element, matches[1]);
|
||||
return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]);
|
||||
}
|
||||
},
|
||||
|
||||
handlers: {
|
||||
// UTILITY FUNCTIONS
|
||||
// joins two collections
|
||||
concat: function(a, b) {
|
||||
for (var i = 0, node; node = b[i]; i++)
|
||||
a.push(node);
|
||||
return a;
|
||||
},
|
||||
|
||||
// marks an array of nodes for counting
|
||||
mark: function(nodes) {
|
||||
var _true = Prototype.emptyFunction;
|
||||
for (var i = 0, node; node = nodes[i]; i++)
|
||||
node._countedByPrototype = _true;
|
||||
return nodes;
|
||||
},
|
||||
|
||||
unmark: function(nodes) {
|
||||
for (var i = 0, node; node = nodes[i]; i++)
|
||||
node._countedByPrototype = undefined;
|
||||
return nodes;
|
||||
},
|
||||
|
||||
// mark each child node with its position (for nth calls)
|
||||
// "ofType" flag indicates whether we're indexing for nth-of-type
|
||||
// rather than nth-child
|
||||
index: function(parentNode, reverse, ofType) {
|
||||
parentNode._countedByPrototype = Prototype.emptyFunction;
|
||||
if (reverse) {
|
||||
for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {
|
||||
var node = nodes[i];
|
||||
if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
|
||||
}
|
||||
} else {
|
||||
for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)
|
||||
if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
|
||||
}
|
||||
},
|
||||
|
||||
// filters out duplicates and extends all nodes
|
||||
unique: function(nodes) {
|
||||
if (nodes.length == 0) return nodes;
|
||||
var results = [], n;
|
||||
for (var i = 0, l = nodes.length; i < l; i++)
|
||||
if (!(n = nodes[i])._countedByPrototype) {
|
||||
n._countedByPrototype = Prototype.emptyFunction;
|
||||
results.push(Element.extend(n));
|
||||
}
|
||||
return Selector.handlers.unmark(results);
|
||||
},
|
||||
|
||||
// COMBINATOR FUNCTIONS
|
||||
descendant: function(nodes) {
|
||||
var h = Selector.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;
|
||||
for (var i = 0, results = [], node; node = nodes[i]; i++) {
|
||||
for (var j = 0, child; child = node.childNodes[j]; j++)
|
||||
if (child.nodeType == 1 && child.tagName != '!') results.push(child);
|
||||
}
|
||||
return results;
|
||||
},
|
||||
|
||||
adjacent: function(nodes) {
|
||||
for (var i = 0, results = [], node; node = nodes[i]; i++) {
|
||||
var next = this.nextElementSibling(node);
|
||||
if (next) results.push(next);
|
||||
}
|
||||
return results;
|
||||
},
|
||||
|
||||
laterSibling: function(nodes) {
|
||||
var h = Selector.handlers;
|
||||
for (var i = 0, results = [], node; node = nodes[i]; i++)
|
||||
h.concat(results, Element.nextSiblings(node));
|
||||
return results;
|
||||
},
|
||||
|
||||
nextElementSibling: function(node) {
|
||||
while (node = node.nextSibling)
|
||||
if (node.nodeType == 1) return node;
|
||||
return null;
|
||||
},
|
||||
|
||||
previousElementSibling: function(node) {
|
||||
while (node = node.previousSibling)
|
||||
if (node.nodeType == 1) return node;
|
||||
return null;
|
||||
},
|
||||
|
||||
// TOKEN FUNCTIONS
|
||||
tagName: function(nodes, root, tagName, combinator) {
|
||||
var uTagName = tagName.toUpperCase();
|
||||
var results = [], h = Selector.handlers;
|
||||
if (nodes) {
|
||||
if (combinator) {
|
||||
// fastlane for ordinary descendant combinators
|
||||
if (combinator == "descendant") {
|
||||
for (var i = 0, node; node = nodes[i]; i++)
|
||||
h.concat(results, node.getElementsByTagName(tagName));
|
||||
return results;
|
||||
} else nodes = this[combinator](nodes);
|
||||
if (tagName == "*") return nodes;
|
||||
}
|
||||
for (var i = 0, node; node = nodes[i]; i++)
|
||||
if (node.tagName.toUpperCase() === uTagName) results.push(node);
|
||||
return results;
|
||||
} else return root.getElementsByTagName(tagName);
|
||||
},
|
||||
|
||||
id: function(nodes, root, id, combinator) {
|
||||
var targetNode = $(id), h = Selector.handlers;
|
||||
if (!targetNode) return [];
|
||||
if (!nodes && root == document) return [targetNode];
|
||||
if (nodes) {
|
||||
if (combinator) {
|
||||
if (combinator == 'child') {
|
||||
for (var i = 0, node; node = nodes[i]; i++)
|
||||
if (targetNode.parentNode == node) return [targetNode];
|
||||
} else if (combinator == 'descendant') {
|
||||
for (var i = 0, node; node = nodes[i]; i++)
|
||||
if (Element.descendantOf(targetNode, node)) return [targetNode];
|
||||
} else if (combinator == 'adjacent') {
|
||||
for (var i = 0, node; node = nodes[i]; i++)
|
||||
if (Selector.handlers.previousElementSibling(targetNode) == node)
|
||||
return [targetNode];
|
||||
} else nodes = h[combinator](nodes);
|
||||
}
|
||||
for (var i = 0, node; node = nodes[i]; i++)
|
||||
if (node == targetNode) return [targetNode];
|
||||
return [];
|
||||
}
|
||||
return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : [];
|
||||
},
|
||||
|
||||
className: function(nodes, root, className, combinator) {
|
||||
if (nodes && combinator) nodes = this[combinator](nodes);
|
||||
return Selector.handlers.byClassName(nodes, root, className);
|
||||
},
|
||||
|
||||
byClassName: function(nodes, root, className) {
|
||||
if (!nodes) nodes = Selector.handlers.descendant([root]);
|
||||
var needle = ' ' + className + ' ';
|
||||
for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) {
|
||||
nodeClassName = node.className;
|
||||
if (nodeClassName.length == 0) continue;
|
||||
if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle))
|
||||
results.push(node);
|
||||
}
|
||||
return results;
|
||||
},
|
||||
|
||||
attrPresence: function(nodes, root, attr, combinator) {
|
||||
if (!nodes) nodes = root.getElementsByTagName("*");
|
||||
if (nodes && combinator) nodes = this[combinator](nodes);
|
||||
var results = [];
|
||||
for (var i = 0, node; node = nodes[i]; i++)
|
||||
if (Element.hasAttribute(node, attr)) results.push(node);
|
||||
return results;
|
||||
},
|
||||
|
||||
attr: function(nodes, root, attr, value, operator, combinator) {
|
||||
if (!nodes) nodes = root.getElementsByTagName("*");
|
||||
if (nodes && combinator) nodes = this[combinator](nodes);
|
||||
var handler = Selector.operators[operator], results = [];
|
||||
for (var i = 0, node; node = nodes[i]; i++) {
|
||||
var nodeValue = Element.readAttribute(node, attr);
|
||||
if (nodeValue === null) continue;
|
||||
if (handler(nodeValue, value)) results.push(node);
|
||||
}
|
||||
return results;
|
||||
},
|
||||
|
||||
pseudo: function(nodes, name, value, root, combinator) {
|
||||
if (nodes && combinator) nodes = this[combinator](nodes);
|
||||
if (!nodes) nodes = root.getElementsByTagName("*");
|
||||
return Selector.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;
|
||||
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;
|
||||
results.push(node);
|
||||
}
|
||||
return results;
|
||||
},
|
||||
'only-child': function(nodes, value, root) {
|
||||
var h = Selector.handlers;
|
||||
for (var i = 0, results = [], node; node = nodes[i]; i++)
|
||||
if (!h.previousElementSibling(node) && !h.nextElementSibling(node))
|
||||
results.push(node);
|
||||
return results;
|
||||
},
|
||||
'nth-child': function(nodes, formula, root) {
|
||||
return 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);
|
||||
},
|
||||
'only-of-type': function(nodes, formula, root) {
|
||||
var p = Selector.pseudos;
|
||||
return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root);
|
||||
},
|
||||
|
||||
// handles the an+b logic
|
||||
getIndices: function(a, b, total) {
|
||||
if (a == 0) return b > 0 ? [b] : [];
|
||||
return $R(1, total).inject([], function(memo, i) {
|
||||
if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i);
|
||||
return memo;
|
||||
});
|
||||
},
|
||||
|
||||
// handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type
|
||||
nth: function(nodes, formula, root, reverse, ofType) {
|
||||
if (nodes.length == 0) return [];
|
||||
if (formula == 'even') formula = '2n+0';
|
||||
if (formula == 'odd') formula = '2n+1';
|
||||
var h = Selector.handlers, results = [], indexed = [], m;
|
||||
h.mark(nodes);
|
||||
for (var i = 0, node; node = nodes[i]; i++) {
|
||||
if (!node.parentNode._countedByPrototype) {
|
||||
h.index(node.parentNode, reverse, ofType);
|
||||
indexed.push(node.parentNode);
|
||||
}
|
||||
}
|
||||
if (formula.match(/^\d+$/)) { // just a number
|
||||
formula = Number(formula);
|
||||
for (var i = 0, node; node = nodes[i]; i++)
|
||||
if (node.nodeIndex == formula) results.push(node);
|
||||
} else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
|
||||
if (m[1] == "-") m[1] = -1;
|
||||
var a = m[1] ? Number(m[1]) : 1;
|
||||
var b = m[2] ? Number(m[2]) : 0;
|
||||
var indices = Selector.pseudos.getIndices(a, b, nodes.length);
|
||||
for (var i = 0, node, l = indices.length; node = nodes[i]; i++) {
|
||||
for (var j = 0; j < l; j++)
|
||||
if (node.nodeIndex == indices[j]) results.push(node);
|
||||
}
|
||||
}
|
||||
h.unmark(nodes);
|
||||
h.unmark(indexed);
|
||||
return results;
|
||||
},
|
||||
|
||||
'empty': function(nodes, value, root) {
|
||||
for (var i = 0, results = [], node; node = nodes[i]; i++) {
|
||||
// IE treats comments as element nodes
|
||||
if (node.tagName == '!' || node.firstChild) continue;
|
||||
results.push(node);
|
||||
}
|
||||
return results;
|
||||
},
|
||||
|
||||
'not': function(nodes, selector, root) {
|
||||
var h = Selector.handlers, selectorType, m;
|
||||
var exclusions = new Selector(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) {
|
||||
expressions.push(m[1].strip());
|
||||
});
|
||||
return expressions;
|
||||
},
|
||||
|
||||
matchElements: function(elements, expression) {
|
||||
var matches = $$(expression), h = Selector.handlers;
|
||||
h.mark(matches);
|
||||
for (var i = 0, results = [], element; element = elements[i]; i++)
|
||||
if (element._countedByPrototype) results.push(element);
|
||||
h.unmark(matches);
|
||||
return results;
|
||||
},
|
||||
|
||||
findElement: function(elements, expression, index) {
|
||||
if (Object.isNumber(expression)) {
|
||||
index = expression; expression = false;
|
||||
}
|
||||
return Selector.matchElements(elements, expression || '*')[index || 0];
|
||||
},
|
||||
|
||||
findChildElements: function(element, expressions) {
|
||||
expressions = Selector.split(expressions.join(','));
|
||||
var results = [], h = Selector.handlers;
|
||||
for (var i = 0, l = expressions.length, selector; i < l; i++) {
|
||||
selector = new Selector(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));
|
||||
}
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* == Language ==
|
||||
* Additions to JavaScript's "standard library" and extensions to
|
||||
* built-in JavaScript objects.
|
||||
**/
|
||||
|
||||
var Abstract = { };
|
||||
|
||||
/** section: Language
|
||||
* 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"
|
|
@ -1,64 +1,205 @@
|
|||
/** 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.
|
||||
**/
|
||||
function $A(iterable) {
|
||||
if (!iterable) return [];
|
||||
if (iterable.toArray) return iterable.toArray();
|
||||
// Safari <2.0.4 crashes when accessing property of a node list with property accessor.
|
||||
// It nevertheless works fine with `in` operator, which is why we use it here
|
||||
if ('toArray' in Object(iterable)) return iterable.toArray();
|
||||
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: Language, related to: Array
|
||||
* $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+/) : [];
|
||||
}
|
||||
|
||||
/** 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.
|
||||
*
|
||||
* <h5>Why you should stop using for...in to iterate</h5>
|
||||
*
|
||||
* 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).
|
||||
*
|
||||
* <h5>What you should use instead</h5>
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
*
|
||||
* <h5>A note on performance</h5>
|
||||
*
|
||||
* 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
|
||||
|
||||
|
||||
function each(iterator) {
|
||||
for (var i = 0, length = this.length; i < length; i++)
|
||||
iterator(this[i]);
|
||||
}
|
||||
if (!_each) _each = each;
|
||||
|
||||
|
||||
/**
|
||||
* Array#clear() -> Array
|
||||
*
|
||||
* Clears the array (makes it empty) and returns the array reference.
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* 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))
|
||||
|
@ -68,6 +209,21 @@ Array.from = $A;
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* <h5>Examples</h5>
|
||||
*
|
||||
* [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) {
|
||||
|
@ -75,14 +231,49 @@ Array.from = $A;
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* <h5>Examples</h5>
|
||||
*
|
||||
* // 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 : this.toArray())._reverse();
|
||||
}
|
||||
|
||||
function reduce() {
|
||||
return this.length > 1 ? this : this[0];
|
||||
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.
|
||||
*
|
||||
* <h5>Examples</h5>
|
||||
*
|
||||
* [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)))
|
||||
|
@ -91,24 +282,68 @@ Array.from = $A;
|
|||
});
|
||||
}
|
||||
|
||||
function intersect(array) {
|
||||
return this.uniq().findAll(function(item) {
|
||||
/**
|
||||
* 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#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.
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* ['Apples', {good: 'yes', bad: 'no'}, 3, 34].inspect()
|
||||
* // -> "['Apples', [object Object], 3, 34]"
|
||||
**/
|
||||
function inspect() {
|
||||
return '[' + this.map(Object.inspect).join(', ') + ']';
|
||||
}
|
||||
|
||||
/** related to: Object.toJSON
|
||||
* Array#toJSON() -> String
|
||||
*
|
||||
* Returns a JSON string representation of the array.
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* ['a', {b: null}].toJSON();
|
||||
* //-> '["a", {"b": null}]'
|
||||
**/
|
||||
function toJSON() {
|
||||
var results = [];
|
||||
this.each(function(object) {
|
||||
|
@ -117,7 +352,28 @@ Array.from = $A;
|
|||
});
|
||||
return '[' + results.join(', ') + ']';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Array#indexOf(item[, offset = 0]) -> Number
|
||||
* - item (?): A value that may or may not be in the array.
|
||||
* - offset (Number): The number of initial items to skip before beginning
|
||||
* the search.
|
||||
*
|
||||
* Returns the index of the first occurrence of `item` within the array,
|
||||
* or `-1` if `item` doesn't exist in the array. `Array#indexOf` compares
|
||||
* items using *strict equality* (`===`).
|
||||
*
|
||||
* <h5>Examples</h5>
|
||||
*
|
||||
* [3, 5, 6, 1, 20].indexOf(1)
|
||||
* // -> 3
|
||||
*
|
||||
* [3, 5, 6, 1, 20].indexOf(90)
|
||||
* // -> -1 (not found)
|
||||
*
|
||||
* ['1', '2', '3'].indexOf(1);
|
||||
* // -> -1 (not found, 1 !== '1')
|
||||
**/
|
||||
function indexOf(item, i) {
|
||||
i || (i = 0);
|
||||
var length = this.length;
|
||||
|
@ -126,32 +382,42 @@ Array.from = $A;
|
|||
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;
|
||||
}
|
||||
|
||||
|
||||
// Replaces a built-in function. No PDoc needed.
|
||||
function concat() {
|
||||
var array = [];
|
||||
for (var i = 0, length = this.length; i < length; i++) array.push(this[i]);
|
||||
var array = slice.call(this, 0), item;
|
||||
for (var i = 0, length = arguments.length; i < length; i++) {
|
||||
if (Object.isArray(arguments[i])) {
|
||||
for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++)
|
||||
array.push(arguments[i][j]);
|
||||
} else {
|
||||
array.push(arguments[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,
|
||||
|
@ -161,7 +427,6 @@ Array.from = $A;
|
|||
flatten: flatten,
|
||||
without: without,
|
||||
reverse: reverse,
|
||||
reduce: reduce,
|
||||
uniq: uniq,
|
||||
intersect: intersect,
|
||||
clone: clone,
|
||||
|
@ -170,9 +435,15 @@ Array.from = $A;
|
|||
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 (!'indexOf' in arrayProto) arrayProto.indexOf = indexOf;
|
||||
if (!'lastIndexOf' in arrayProto) arrayProto.lastIndexOf = lastIndexOf;
|
||||
if (!'concat' in arrayProto) arrayProto.concat = concat;
|
||||
if (!arrayProto.indexOf) arrayProto.indexOf = indexOf;
|
||||
if (!arrayProto.lastIndexOf) arrayProto.lastIndexOf = lastIndexOf;
|
||||
})();
|
||||
|
|
|
@ -1,46 +1,168 @@
|
|||
/* Based on Alex Arnell's inheritance implementation. */
|
||||
|
||||
/** section: Language
|
||||
* 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() {
|
||||
|
||||
// Some versions of JScript fail to enumerate over properties, names of which
|
||||
// correspond to non-enumerable properties in the prototype chain
|
||||
var IS_DONTENUM_BUGGY = (function(){
|
||||
for (var p in { toString: 1 }) {
|
||||
// check actual property name, so that it works with augmented Object.prototype
|
||||
if (p === 'toString') return false;
|
||||
}
|
||||
return true;
|
||||
})();
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
var subclass = function() {};
|
||||
subclass.prototype = parent.prototype;
|
||||
klass.prototype = new subclass;
|
||||
parent.subclasses.push(klass);
|
||||
}
|
||||
|
||||
for (var i = 0; i < properties.length; i++)
|
||||
|
||||
for (var i = 0, length = properties.length; i < length; i++)
|
||||
klass.addMethods(properties[i]);
|
||||
|
||||
|
||||
if (!klass.prototype.initialize)
|
||||
klass.prototype.initialize = Prototype.emptyFunction;
|
||||
|
||||
|
||||
klass.prototype.constructor = klass;
|
||||
return 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.
|
||||
*
|
||||
* <h5>Examples</h5>
|
||||
*
|
||||
* 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;
|
||||
var properties = Object.keys(source);
|
||||
|
||||
if (!Object.keys({ toString: true }).length)
|
||||
properties.push("toString", "valueOf");
|
||||
|
||||
var ancestor = this.superclass && this.superclass.prototype,
|
||||
properties = Object.keys(source);
|
||||
|
||||
// IE6 doesn't enumerate `toString` and `valueOf` (among other built-in `Object.prototype`) properties,
|
||||
// Force copy if they're not Object.prototype ones.
|
||||
// Do not copy other Object.prototype.* for performance reasons
|
||||
if (IS_DONTENUM_BUGGY) {
|
||||
if (source.toString != Object.prototype.toString)
|
||||
properties.push("toString");
|
||||
if (source.valueOf != Object.prototype.valueOf)
|
||||
properties.push("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") {
|
||||
value.argumentNames()[0] == "$super") {
|
||||
var method = value;
|
||||
value = (function(m) {
|
||||
return function() { return ancestor[m].apply(this, arguments); };
|
||||
|
@ -51,14 +173,14 @@ var Class = (function() {
|
|||
}
|
||||
this.prototype[property] = value;
|
||||
}
|
||||
|
||||
return this;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
create: create,
|
||||
Methods: {
|
||||
addMethods: addMethods
|
||||
}
|
||||
};
|
||||
})();
|
||||
})();
|
||||
|
|
|
@ -1,3 +1,23 @@
|
|||
/** section: Language
|
||||
* class Date
|
||||
*
|
||||
* Extensions to the built-in `Date` object.
|
||||
**/
|
||||
|
||||
/**
|
||||
* Date#toJSON() -> String
|
||||
*
|
||||
* Produces a string representation of the date in ISO 8601 format.
|
||||
* The time zone is always UTC, as denoted by the suffix "Z".
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* var d = new Date(1969, 11, 31, 19);
|
||||
* d.getTimezoneOffset();
|
||||
* //-> -180 (time offest is given in minutes.)
|
||||
* d.toJSON();
|
||||
* //-> '"1969-12-31T16:00:00Z"'
|
||||
**/
|
||||
Date.prototype.toJSON = function() {
|
||||
return '"' + this.getUTCFullYear() + '-' +
|
||||
(this.getUTCMonth() + 1).toPaddedString(2) + '-' +
|
||||
|
|
|
@ -1,6 +1,111 @@
|
|||
/** 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 standaone
|
||||
* 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.
|
||||
*
|
||||
* <h5>The <code>context</code> parameter</h5>
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* <h5>Mixing <code>Enumerable</code> into your own objects</h5>
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* <h5>Examples</h5>
|
||||
*
|
||||
* ['one', 'two', 'three'].each(alert);
|
||||
* // Alerts "one", then alerts "two", then alerts "three"
|
||||
*
|
||||
* <h5>Built-In Variants</h5>
|
||||
*
|
||||
* 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) {
|
||||
var index = 0;
|
||||
try {
|
||||
|
@ -12,7 +117,35 @@ var Enumerable = (function() {
|
|||
}
|
||||
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]].
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* 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;
|
||||
|
@ -21,6 +154,34 @@ var Enumerable = (function() {
|
|||
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`);
|
||||
*
|
||||
* <h5>Examples</h5>
|
||||
*
|
||||
* [].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;
|
||||
|
@ -31,6 +192,28 @@ var Enumerable = (function() {
|
|||
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.
|
||||
*
|
||||
* <h5>Examples</h5>
|
||||
*
|
||||
* [].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;
|
||||
|
@ -41,6 +224,29 @@ var Enumerable = (function() {
|
|||
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.
|
||||
*
|
||||
* <h5>Examples</h5>
|
||||
*
|
||||
* ['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 = [];
|
||||
|
@ -49,7 +255,22 @@ var Enumerable = (function() {
|
|||
});
|
||||
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.
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* [1, 7, -2, -4, 5].detect(function(n) { return n < 0; });
|
||||
* // -> -2
|
||||
**/
|
||||
function detect(iterator, context) {
|
||||
var result;
|
||||
this.each(function(value, index) {
|
||||
|
@ -60,7 +281,21 @@ var Enumerable = (function() {
|
|||
});
|
||||
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]].
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* [1, 'two', 3, 'four', 5].findAll(Object.isString);
|
||||
* // -> ['two', 'four']
|
||||
**/
|
||||
function findAll(iterator, context) {
|
||||
var results = [];
|
||||
this.each(function(value, index) {
|
||||
|
@ -69,21 +304,71 @@ var Enumerable = (function() {
|
|||
});
|
||||
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.
|
||||
*
|
||||
* <h5>Examples</h5>
|
||||
*
|
||||
* // 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(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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*
|
||||
* <h5>Examples</h5>
|
||||
*
|
||||
* $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))
|
||||
if (this.indexOf(object) != -1) return true;
|
||||
|
@ -97,7 +382,33 @@ var Enumerable = (function() {
|
|||
});
|
||||
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.
|
||||
*
|
||||
* <h5>Examples</h5>
|
||||
*
|
||||
* 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) {
|
||||
|
@ -105,21 +416,100 @@ var Enumerable = (function() {
|
|||
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.
|
||||
*
|
||||
* <h5>Examples</h5>
|
||||
*
|
||||
* $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);
|
||||
});
|
||||
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.
|
||||
*
|
||||
* <h5>Examples</h5>
|
||||
*
|
||||
* ['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).
|
||||
*
|
||||
* <h5>Examples</h5>
|
||||
*
|
||||
* ['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;
|
||||
|
@ -130,7 +520,36 @@ var Enumerable = (function() {
|
|||
});
|
||||
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).
|
||||
*
|
||||
* <h5>Examples</h5>
|
||||
*
|
||||
* ['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;
|
||||
|
@ -141,7 +560,34 @@ var Enumerable = (function() {
|
|||
});
|
||||
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.
|
||||
*
|
||||
* <h5>Examples</h5>
|
||||
*
|
||||
* ['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 = [];
|
||||
|
@ -151,7 +597,20 @@ var Enumerable = (function() {
|
|||
});
|
||||
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.
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* ['hello', 'world', 'this', 'is', 'nice'].pluck('length');
|
||||
* // -> [5, 5, 4, 2, 4]
|
||||
**/
|
||||
function pluck(property) {
|
||||
var results = [];
|
||||
this.each(function(value) {
|
||||
|
@ -159,7 +618,21 @@ var Enumerable = (function() {
|
|||
});
|
||||
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]].
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* [1, "two", 3, "four", 5].reject(Object.isString);
|
||||
* // -> [1, 3, 5]
|
||||
**/
|
||||
function reject(iterator, context) {
|
||||
var results = [];
|
||||
this.each(function(value, index) {
|
||||
|
@ -168,7 +641,29 @@ var Enumerable = (function() {
|
|||
});
|
||||
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).
|
||||
*
|
||||
* `sortBy` does not guarantee a *stable* sort; adjacent equivalent elements
|
||||
* may be swapped.
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* ['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 {
|
||||
|
@ -180,11 +675,56 @@ var Enumerable = (function() {
|
|||
return a < b ? -1 : a > b ? 1 : 0;
|
||||
}).pluck('value');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Enumerable#toArray() -> Array
|
||||
*
|
||||
* Returns an Array containing the elements of the enumeration.
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* $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.
|
||||
*
|
||||
* <h5>Examples</h5>
|
||||
*
|
||||
* 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()))
|
||||
|
@ -195,15 +735,57 @@ var Enumerable = (function() {
|
|||
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 '#<Enumerable:' + this.toArray().inspect() + '>';
|
||||
}
|
||||
|
||||
|
||||
/** 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,
|
||||
|
|
|
@ -1,17 +1,40 @@
|
|||
/** 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).
|
||||
*
|
||||
* <h5>Examples</h5>
|
||||
*
|
||||
* 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, '')
|
||||
|
@ -19,6 +42,69 @@ Object.extend(Function.prototype, (function() {
|
|||
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.
|
||||
*
|
||||
* <h5>Examples</h5>
|
||||
*
|
||||
* 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;
|
||||
var __method = this, args = slice.call(arguments, 1);
|
||||
|
@ -28,6 +114,61 @@ Object.extend(Function.prototype, (function() {
|
|||
}
|
||||
}
|
||||
|
||||
/** 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.
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* 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 <provisional>',
|
||||
* 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) {
|
||||
|
@ -36,6 +177,30 @@ Object.extend(Function.prototype, (function() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
@ -45,19 +210,108 @@ Object.extend(Function.prototype, (function() {
|
|||
}
|
||||
}
|
||||
|
||||
function delay(timeout) {
|
||||
/**
|
||||
* 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]].
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* 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
|
||||
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.
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* 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 wraper 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.)
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* // 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() {
|
||||
|
@ -66,6 +320,54 @@ Object.extend(Function.prototype, (function() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* // 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;
|
||||
|
@ -74,7 +376,7 @@ Object.extend(Function.prototype, (function() {
|
|||
return __method.apply(null, a);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
argumentNames: argumentNames,
|
||||
bind: bind,
|
||||
|
|
271
src/lang/hash.js
271
src/lang/hash.js
|
@ -1,12 +1,85 @@
|
|||
/** section: Language, related to: Hash
|
||||
* $H([object]) -> Hash
|
||||
*
|
||||
* Creates a `Hash`. This is purely a convenience wrapper around the Hash
|
||||
* constructor, it does not do anything other than pass any argument it's
|
||||
* given into the Hash constructor and return the result.
|
||||
**/
|
||||
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.
|
||||
*
|
||||
* <h5>Creating a hash</h5>
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* 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) {
|
||||
for (var key in this._object) {
|
||||
var value = this._object[key], pair = [key, value];
|
||||
|
@ -16,45 +89,174 @@ var Hash = Class.create(Enumerable, (function() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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`.
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* 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`.
|
||||
*
|
||||
* <h5>Examples</h5>
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* var h = new Hash({ a: 'apple', b: 'banana', c: 'coconut' });
|
||||
* var obj = h.toObject();
|
||||
* obj.a;
|
||||
* // -> "apple"
|
||||
**/
|
||||
function toObject() {
|
||||
return Object.clone(this._object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash#keys() -> [String...]
|
||||
*
|
||||
* Provides an Array containing the keys for items stored in the hash.
|
||||
*
|
||||
* The order of the keys is not guaranteed.
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* 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;
|
||||
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]].
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* `update` modifies the hash. To get a new hash instead, use
|
||||
* [[Hash#merge]].
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* 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);
|
||||
|
@ -62,15 +264,55 @@ var Hash = Class.create(Enumerable, (function() {
|
|||
});
|
||||
}
|
||||
|
||||
// Private. No PDoc necessary.
|
||||
function toQueryPair(key, value) {
|
||||
if (Object.isUndefined(value)) return key;
|
||||
return key + '=' + encodeURIComponent(String.interpret(value));
|
||||
}
|
||||
|
||||
/** 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.
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* $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))
|
||||
return results.concat(values.map(toQueryPair.curry(key)));
|
||||
|
@ -79,20 +321,41 @@ var Hash = Class.create(Enumerable, (function() {
|
|||
}).join('&');
|
||||
}
|
||||
|
||||
/** related to: Object.inspect
|
||||
* Hash#inspect() -> String
|
||||
*
|
||||
* Returns the debug-oriented string representation of the Hash.
|
||||
**/
|
||||
function inspect() {
|
||||
return '#<Hash:{' + this.map(function(pair) {
|
||||
return pair.map(Object.inspect).join(': ');
|
||||
}).join(', ') + '}>';
|
||||
}
|
||||
|
||||
/** related to: Object.toJSON
|
||||
* Hash#toJSON() -> String
|
||||
*
|
||||
* Returns a JSON string containing the keys and values in this hash.
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* var h = $H({'a': 'apple', 'b': 23, 'c': false});
|
||||
* h.toJSON();
|
||||
* // -> {"a": "apple", "b": 23, "c": false}
|
||||
**/
|
||||
function toJSON() {
|
||||
return Object.toJSON(this.toObject());
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash#clone() -> Hash
|
||||
*
|
||||
* Returns a clone of this Hash.
|
||||
**/
|
||||
function clone() {
|
||||
return new Hash(this);
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
initialize: initialize,
|
||||
_each: _each,
|
||||
|
|
|
@ -1,42 +1,159 @@
|
|||
/** 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.
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* (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.
|
||||
*
|
||||
* <h5>Examples</h5>
|
||||
*
|
||||
* (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;
|
||||
}
|
||||
|
||||
|
||||
/** related to: Object.toJSON
|
||||
* Number#toJSON() -> String
|
||||
*
|
||||
* Returns a JSON string representation of the number.
|
||||
**/
|
||||
function toJSON() {
|
||||
return isFinite(this) ? this.toString() : 'null';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Number#abs() -> Number
|
||||
*
|
||||
* 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,
|
||||
|
|
|
@ -1,10 +1,47 @@
|
|||
/** 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.
|
||||
*
|
||||
**/
|
||||
(function() {
|
||||
|
||||
var _toString = Object.prototype.toString;
|
||||
|
||||
/**
|
||||
* 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';
|
||||
|
@ -16,6 +53,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
|
@ -39,57 +89,189 @@
|
|||
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 keys = [];
|
||||
var results = [];
|
||||
for (var property in object)
|
||||
keys.push(property);
|
||||
return keys;
|
||||
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 values = [];
|
||||
var results = [];
|
||||
for (var property in object)
|
||||
values.push(object[property]);
|
||||
return values;
|
||||
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.
|
||||
*
|
||||
* <h5>Examples</h5>
|
||||
*
|
||||
* 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 was a shallow copy, so they shared 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.
|
||||
**/
|
||||
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 object != null && typeof object === "object" &&
|
||||
'splice' in object && 'join' in object;
|
||||
return _toString.call(object) == "[object Array]";
|
||||
}
|
||||
|
||||
var hasNativeIsArray = (typeof Array.isArray == 'function')
|
||||
&& Array.isArray([]) && !Array.isArray({});
|
||||
|
||||
if (hasNativeIsArray) {
|
||||
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.
|
||||
**/
|
||||
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 typeof object === "string";
|
||||
return _toString.call(object) == "[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 typeof object === "number";
|
||||
return _toString.call(object) == "[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";
|
||||
}
|
||||
|
|
|
@ -1,4 +1,25 @@
|
|||
/** section: Language
|
||||
* class PeriodicalExecuter
|
||||
*
|
||||
* A class that oversees the calling of a particular function periodically.
|
||||
*
|
||||
* `PeriodicalExecuter` shields you from multiple parallel executions of the
|
||||
* `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.
|
||||
**/
|
||||
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 `PeriodicalExecuter`.
|
||||
**/
|
||||
initialize: function(callback, frequency) {
|
||||
this.callback = callback;
|
||||
this.frequency = frequency;
|
||||
|
@ -14,7 +35,12 @@ var PeriodicalExecuter = Class.create({
|
|||
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);
|
||||
|
@ -23,11 +49,18 @@ var PeriodicalExecuter = Class.create({
|
|||
|
||||
onTimerEvent: function() {
|
||||
if (!this.currentlyExecuting) {
|
||||
// IE doesn't support `finally` statements unless all errors are caught.
|
||||
// We mimic the behaviour of `finally` statements by duplicating code
|
||||
// that would belong in it. First at the bottom of the `try` statement
|
||||
// (for errorless cases). Secondly, inside a `catch` statement which
|
||||
// rethrows any caught errors.
|
||||
try {
|
||||
this.currentlyExecuting = true;
|
||||
this.execute();
|
||||
} finally {
|
||||
this.currentlyExecuting = false;
|
||||
} catch(e) {
|
||||
this.currentlyExecuting = false;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,51 @@
|
|||
/** 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).
|
||||
**/
|
||||
|
||||
/** 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.
|
||||
**/
|
||||
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) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.exclusive = exclusive;
|
||||
}
|
||||
|
||||
|
||||
function _each(iterator) {
|
||||
var value = this.start;
|
||||
while (this.include(value)) {
|
||||
|
@ -16,15 +53,20 @@ var ObjectRange = Class.create(Enumerable, (function() {
|
|||
value = value.succ();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ObjectRange#include(value) -> Boolean
|
||||
*
|
||||
* Determines whether the value is included in the range.
|
||||
**/
|
||||
function include(value) {
|
||||
if (value < this.start)
|
||||
if (value < this.start)
|
||||
return false;
|
||||
if (this.exclusive)
|
||||
return value < this.end;
|
||||
return value <= this.end;
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
initialize: initialize,
|
||||
_each: _each,
|
||||
|
|
|
@ -1,5 +1,25 @@
|
|||
/** 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.
|
||||
**/
|
||||
RegExp.prototype.match = RegExp.prototype.test;
|
||||
|
||||
/**
|
||||
* RegExp.escape(str) -> String
|
||||
* - str (String): A string intended to be used in a `RegExp` constructor.
|
||||
*
|
||||
* Escapes any characters in the string that have special meaning in a
|
||||
* regular expression.
|
||||
*
|
||||
* Use before passing a string into the `RegExp` constructor.
|
||||
**/
|
||||
RegExp.escape = function(str) {
|
||||
return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
|
||||
};
|
||||
|
|
|
@ -1,4 +1,20 @@
|
|||
/** 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]].
|
||||
**/
|
||||
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);
|
||||
},
|
||||
|
@ -13,17 +29,37 @@ Object.extend(String, {
|
|||
});
|
||||
|
||||
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.
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* ""hello".gsub(/([aeiou])/, '<#{1}>');
|
||||
* // => "h<e>ll<o>"
|
||||
**/
|
||||
function gsub(pattern, replacement) {
|
||||
var result = '', source = this, match;
|
||||
replacement = prepareReplacement(replacement);
|
||||
|
||||
if (Object.isString(pattern))
|
||||
pattern = RegExp.escape(pattern);
|
||||
|
||||
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);
|
||||
|
@ -36,6 +72,18 @@ Object.extend(String.prototype, (function() {
|
|||
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.
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* "20091201".sub(/^(\d{4})(\d{2})(\d{2})$/, "#{1}-#{2}-#{3}");
|
||||
* // => "2009-12-01"
|
||||
**/
|
||||
function sub(pattern, replacement, count) {
|
||||
replacement = prepareReplacement(replacement);
|
||||
count = Object.isUndefined(count) ? 1 : count;
|
||||
|
@ -46,64 +94,176 @@ Object.extend(String.prototype, (function() {
|
|||
});
|
||||
}
|
||||
|
||||
/** 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 ?
|
||||
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 tags.
|
||||
*
|
||||
* Note that `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`.
|
||||
*
|
||||
* <h5>Caveat User</h5>
|
||||
*
|
||||
* Note that the processing `stripTags` does is good enough for most purposes, but
|
||||
* you cannot rely on it for security purposes. If you're processing end-user-supplied
|
||||
* content, `stripTags` is _not_ sufficiently robust to ensure that the content
|
||||
* is completely devoid of HTML tags in the case of a user intentionally trying to circumvent
|
||||
* tag restrictions. But then, you'll be running them through [[String#escapeHTML]] anyway,
|
||||
* won't you?
|
||||
**/
|
||||
function stripTags() {
|
||||
return this.replace(/<\/?[^>]+>/gi, '');
|
||||
return this.replace(/<\w+(\s+("[^"]*"|'[^']*'|[^>])+)?>|<\/\w+>/gi, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* String#stripScripts() -> String
|
||||
*
|
||||
* Strips a string of things that look like an HTML script blocks.
|
||||
*
|
||||
* <h5>Example</h5>
|
||||
*
|
||||
* "<p>This is a test.<script>alert("Look, a test!");</script>End of test</p>".stripScripts();
|
||||
* // => "<p>This is a test.End of test</p>"
|
||||
*
|
||||
* <h5>Caveat User</h5>
|
||||
*
|
||||
* Note that the processing `stripScripts` does is good enough for most purposes,
|
||||
* but you cannot rely on it for security purposes. If you're processing end-user-supplied
|
||||
* content, `stripScripts` is probably not sufficiently robust to prevent hack attacks.
|
||||
**/
|
||||
function stripScripts() {
|
||||
return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
|
||||
}
|
||||
|
||||
/**
|
||||
* String#extractScripts() -> Array
|
||||
*
|
||||
* Extracts the content of any script blocks present in the string and
|
||||
* returns them as an array of strings.
|
||||
**/
|
||||
function extractScripts() {
|
||||
var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
|
||||
var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
|
||||
var matchAll = new RegExp(Prototype.ScriptFragment, 'img'),
|
||||
matchOne = new RegExp(Prototype.ScriptFragment, 'im');
|
||||
return (this.match(matchAll) || []).map(function(scriptTag) {
|
||||
return (scriptTag.match(matchOne) || ['', ''])[1];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* String#evalScripts() -> Array
|
||||
*
|
||||
* Evaluates the content of any inline `<script>` block present in the string.
|
||||
* Returns an array containing the value returned by each script.
|
||||
* `<script>` blocks referencing external files will be treated as though
|
||||
* they were empty (the result for that position in the array will be `undefined`);
|
||||
* external files are _not_ loaded and processed by `evalScripts`.
|
||||
*
|
||||
* <h5>About `evalScripts`, `var`s, and defining functions</h5>
|
||||
*
|
||||
* `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 `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 `<script>` blocks _define functions_, they will need to be assigned to
|
||||
* properties of the `window` object.
|
||||
*
|
||||
* For example, this won't work:
|
||||
*
|
||||
* // This kind of script won't work if processed by evalScripts:
|
||||
* function coolFunc() {
|
||||
* // Amazing stuff!
|
||||
* }
|
||||
*
|
||||
* Instead, use the following syntax:
|
||||
*
|
||||
* // This kind of script WILL work if processed by evalScripts:
|
||||
* window.coolFunc = function() {
|
||||
* // Amazing stuff!
|
||||
* }
|
||||
*
|
||||
* (You can leave off the `window.` part of that, but it's bad form.)
|
||||
**/
|
||||
function evalScripts() {
|
||||
return this.extractScripts().map(function(script) { return eval(script) });
|
||||
}
|
||||
|
||||
/**
|
||||
* String#escapeHTML() -> String
|
||||
*
|
||||
* Converts HTML special characters to their entity equivalents.
|
||||
**/
|
||||
function escapeHTML() {
|
||||
var self = arguments.callee;
|
||||
self.text.data = this;
|
||||
return self.div.innerHTML;
|
||||
return this.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
}
|
||||
|
||||
/** 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) : '';
|
||||
// Warning: In 1.7 String#unescapeHTML will no longer call String#stripTags.
|
||||
return this.stripTags().replace(/</g,'<').replace(/>/g,'>').replace(/&/g,'&');
|
||||
}
|
||||
|
||||
/**
|
||||
* String#parseQuery([separator = '&']) -> Object
|
||||
**/
|
||||
|
||||
/** 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];
|
||||
var key = decodeURIComponent(pair.shift()),
|
||||
value = pair.length > 1 ? pair.join('=') : pair[0];
|
||||
|
||||
if (value != undefined) value = decodeURIComponent(value);
|
||||
|
||||
if (key in hash) {
|
||||
|
@ -116,62 +276,131 @@ Object.extend(String.prototype, (function() {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'.
|
||||
*
|
||||
* <h5>Examples</h5>
|
||||
*
|
||||
* 'background-color'.camelize();
|
||||
* // -> 'backgroundColor'
|
||||
*
|
||||
* '-moz-binding'.camelize();
|
||||
* // -> 'MozBinding'
|
||||
**/
|
||||
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;
|
||||
return this.replace(/-+(.)?/g, function(match, chr) {
|
||||
return chr ? chr.toUpperCase() : '';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
return this.replace(/::/g, '/')
|
||||
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
|
||||
.replace(/([a-z\d])([A-Z])/g, '$1_$2')
|
||||
.replace(/-/g, '_')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* String#dasherize() -> String
|
||||
*
|
||||
* Replaces every instance of the underscore character ("_") by a dash ("-").
|
||||
**/
|
||||
function dasherize() {
|
||||
return this.gsub(/_/,'-');
|
||||
return this.replace(/_/g, '-');
|
||||
}
|
||||
|
||||
/** 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);
|
||||
var escapedString = this.replace(/[\x00-\x1f\\]/g, function(character) {
|
||||
if (character in String.specialChar) {
|
||||
return String.specialChar[character];
|
||||
}
|
||||
return '\\u00' + character.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}');
|
||||
return this.replace(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;
|
||||
|
@ -179,6 +408,15 @@ Object.extend(String.prototype, (function() {
|
|||
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; if one is detected, `eval`
|
||||
* is _not called_.
|
||||
**/
|
||||
function evalJSON(sanitize) {
|
||||
var json = this.unfilterJSON();
|
||||
try {
|
||||
|
@ -187,37 +425,75 @@ Object.extend(String.prototype, (function() {
|
|||
throw new SyntaxError('Badly formed JSON string: ' + this.inspect());
|
||||
}
|
||||
|
||||
/**
|
||||
* String#include(substring) -> Boolean
|
||||
*
|
||||
* Checks if the string contains `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;
|
||||
// We use `lastIndexOf` instead of `indexOf` to avoid tying execution
|
||||
// time to string length when string doesn't start with pattern.
|
||||
return this.lastIndexOf(pattern, 0) === 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;
|
||||
// We use `indexOf` instead of `lastIndexOf` to avoid tying execution
|
||||
// time to string length when string doesn't end with pattern.
|
||||
return d >= 0 && this.indexOf(pattern, d) === d;
|
||||
}
|
||||
|
||||
/**
|
||||
* String#empty() -> Boolean
|
||||
*
|
||||
* Checks if the string is empty.
|
||||
**/
|
||||
function empty() {
|
||||
return this == '';
|
||||
}
|
||||
|
||||
/**
|
||||
* String#blank() -> Boolean
|
||||
*
|
||||
* Check if the string is "blank" — either empty (length of `0`) 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,
|
||||
// Firefox 3.5+ supports String.prototype.trim
|
||||
// (`trim` is ~ 5x faster than `strip` in FF3.5)
|
||||
strip: String.prototype.trim || strip,
|
||||
stripTags: stripTags,
|
||||
stripScripts: stripScripts,
|
||||
extractScripts: extractScripts,
|
||||
|
@ -247,19 +523,3 @@ Object.extend(String.prototype, (function() {
|
|||
};
|
||||
})());
|
||||
|
||||
if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, {
|
||||
escapeHTML: function() {
|
||||
return this.replace(/&/g,'&').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);
|
||||
|
||||
|
|
|
@ -1,32 +1,152 @@
|
|||
/** section: Language
|
||||
* class Template
|
||||
*
|
||||
* A class for sophisticated string interpolation.
|
||||
*
|
||||
* Any time you have a group of similar objects and you need to produce
|
||||
* formatted output for these objects, maybe inside a loop, you typically
|
||||
* resort to concatenating string literals with the object's fields:
|
||||
*
|
||||
* "The TV show " + title + " was created by " + author + ".";
|
||||
*
|
||||
* There's nothing wrong with this approach, except that it is hard to
|
||||
* visualize the output immediately just by glancing at the concatenation
|
||||
* expression. The `Template` class provides a much nicer and clearer way of
|
||||
* achieving this formatting.
|
||||
*
|
||||
* <h5>Straightforward templates</h5>
|
||||
*
|
||||
* The `Template` class uses a basic formatting syntax, similar to what is
|
||||
* used in Ruby. The templates are created from strings that have embedded
|
||||
* symbols in the form (e.g., `#{fieldName}`) that will be replaced by
|
||||
* actual values when the template is applied (evaluated) to an object.
|
||||
*
|
||||
* // the template (our formatting expression)
|
||||
* var myTemplate = new Template(
|
||||
* 'The TV show #{title} was created by #{author}.');
|
||||
*
|
||||
* // our data to be formatted by the template
|
||||
* var show = {
|
||||
* title: 'The Simpsons',
|
||||
* author: 'Matt Groening',
|
||||
* network: 'FOX'
|
||||
* };
|
||||
*
|
||||
* // let's format our data
|
||||
* myTemplate.evaluate(show);
|
||||
* // -> "The TV show The Simpsons was created by Matt Groening."
|
||||
*
|
||||
* <h5>Templates are meant to be reused</h5>
|
||||
*
|
||||
* As the example illustrates, `Template` objects are not tied to specific
|
||||
* data. The data is bound to the template only during the evaluation of the
|
||||
* template, without affecting the template itself. The next example shows the
|
||||
* same template being used with a handful of distinct objects.
|
||||
*
|
||||
* // creating a few similar objects
|
||||
* var conversion1 = { from: 'meters', to: 'feet', factor: 3.28 };
|
||||
* var conversion2 = { from: 'kilojoules', to: 'BTUs', factor: 0.9478 };
|
||||
* var conversion3 = { from: 'megabytes', to: 'gigabytes', factor: 1024 };
|
||||
*
|
||||
* // the template
|
||||
* var templ = new Template(
|
||||
* 'Multiply by #{factor} to convert from #{from} to #{to}.');
|
||||
*
|
||||
* // let's format each object
|
||||
* [conversion1, conversion2, conversion3].each( function(conv){
|
||||
* templ.evaluate(conv);
|
||||
* });
|
||||
* // -> Multiply by 3.28 to convert from meters to feet.
|
||||
* // -> Multiply by 0.9478 to convert from kilojoules to BTUs.
|
||||
* // -> Multiply by 1024 to convert from megabytes to gigabytes.
|
||||
*
|
||||
* <h5>Escape sequence</h5>
|
||||
*
|
||||
* There's always the chance that one day you'll need to have a literal in your
|
||||
* template that looks like a symbol, but is not supposed to be replaced. For
|
||||
* these situations there's an escape character: the backslash (<code>\\</code>).
|
||||
*
|
||||
* // NOTE: you're seeing two backslashes here because the backslash
|
||||
* // is also an escape character in JavaScript strings, so a literal
|
||||
* // backslash is represented by two backslashes.
|
||||
* var t = new Template(
|
||||
* 'in #{lang} we also use the \\#{variable} syntax for templates.');
|
||||
* var data = { lang:'Ruby', variable: '(not used)' };
|
||||
* t.evaluate(data);
|
||||
* // -> in Ruby we also use the #{variable} syntax for templates.
|
||||
*
|
||||
* <h5>Custom syntaxes</h5>
|
||||
*
|
||||
* The default syntax of the template strings will probably be enough for most
|
||||
* scenarios. In the rare occasion where the default Ruby-like syntax is
|
||||
* inadequate, there's a provision for customization. `Template`'s
|
||||
* constructor accepts an optional second argument that is a regular expression
|
||||
* object to match the replaceable symbols in the template string. Let's put
|
||||
* together a template that uses a syntax similar to the now ubiquitous `{{ }}`
|
||||
* constructs:
|
||||
*
|
||||
* // matches symbols like '{{ field }}'
|
||||
* var syntax = /(^|.|\r|\n)(\{{\s*(\w+)\s*}})/;
|
||||
*
|
||||
* var t = new Template(
|
||||
* '<div>Name: <b>{{ name }}</b>, Age: <b>{{ age }}</b></div>',
|
||||
* syntax);
|
||||
* t.evaluate( {name: 'John Smith', age: 26} );
|
||||
* // -> <div>Name: <b>John Smith</b>, Age: <b>26</b></div>
|
||||
*
|
||||
* There are important constraints to any custom syntax. Any syntax must
|
||||
* provide at least three groupings in the regular expression. The first
|
||||
* grouping is to capture what comes before the symbol, to detect the backslash
|
||||
* escape character (no, you cannot use a different character). The second
|
||||
* grouping captures the entire symbol and will be completely replaced upon
|
||||
* evaluation. Lastly, the third required grouping captures the name of the
|
||||
* field inside the symbol.
|
||||
*
|
||||
**/
|
||||
var Template = Class.create({
|
||||
/**
|
||||
* new Template(template[, pattern = Template.Pattern])
|
||||
*
|
||||
* Creates a Template object.
|
||||
*
|
||||
* The optional `pattern` argument expects a `RegExp` that defines a custom
|
||||
* syntax for the replaceable symbols in `template`.
|
||||
**/
|
||||
initialize: function(template, pattern) {
|
||||
this.template = template.toString();
|
||||
this.pattern = pattern || Template.Pattern;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Template#evaluate(object) -> String
|
||||
*
|
||||
* Applies the template to `object`'s data, producing a formatted string
|
||||
* with symbols replaced by `object`'s corresponding properties.
|
||||
**/
|
||||
evaluate: function(object) {
|
||||
if (Object.isFunction(object.toTemplateReplacements))
|
||||
if (object && Object.isFunction(object.toTemplateReplacements))
|
||||
object = object.toTemplateReplacements();
|
||||
|
||||
return this.template.gsub(this.pattern, function(match) {
|
||||
if (object == null) return '';
|
||||
|
||||
if (object == null) return (match[1] + '');
|
||||
|
||||
var before = match[1] || '';
|
||||
if (before == '\\') return match[2];
|
||||
|
||||
var ctx = object, expr = match[3];
|
||||
var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/;
|
||||
|
||||
var ctx = object, expr = match[3],
|
||||
pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/;
|
||||
|
||||
match = pattern.exec(expr);
|
||||
if (match == null) return before;
|
||||
|
||||
while (match != null) {
|
||||
var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1];
|
||||
var comp = match[1].startsWith('[') ? match[2].replace(/\\\\]/g, ']') : 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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,72 +1,66 @@
|
|||
<%= include 'HEADER' %>
|
||||
/* 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/)
|
||||
},
|
||||
|
||||
Browser: (function(){
|
||||
var ua = navigator.userAgent;
|
||||
// Opera (at least) 8.x+ has "Opera" as a [[Class]] of `window.opera`
|
||||
// This is a safer inference than plain boolean type conversion of `window.opera`
|
||||
var isOpera = Object.prototype.toString.call(window.opera) == '[object Opera]';
|
||||
return {
|
||||
IE: !!window.attachEvent && !isOpera,
|
||||
Opera: isOpera,
|
||||
WebKit: ua.indexOf('AppleWebKit/') > -1,
|
||||
Gecko: ua.indexOf('Gecko') > -1 && ua.indexOf('KHTML') === -1,
|
||||
MobileSafari: /Apple.*Mobile.*Safari/.test(ua)
|
||||
}
|
||||
})(),
|
||||
|
||||
BrowserFeatures: {
|
||||
XPath: !!document.evaluate,
|
||||
SelectorsAPI: !!document.querySelector,
|
||||
ElementExtensions: !!window.HTMLElement,
|
||||
SpecificElementExtensions:
|
||||
document.createElement('div')['__proto__'] &&
|
||||
document.createElement('div')['__proto__'] !==
|
||||
document.createElement('form')['__proto__']
|
||||
ElementExtensions: (function() {
|
||||
var constructor = window.Element || window.HTMLElement;
|
||||
return !!(constructor && constructor.prototype);
|
||||
})(),
|
||||
SpecificElementExtensions: (function() {
|
||||
// First, try the named class
|
||||
if (typeof window.HTMLDivElement !== 'undefined')
|
||||
return true;
|
||||
|
||||
var div = document.createElement('div'),
|
||||
form = document.createElement('form'),
|
||||
isSupported = false;
|
||||
|
||||
if (div['__proto__'] && (div['__proto__'] !== form['__proto__'])) {
|
||||
isSupported = true;
|
||||
}
|
||||
|
||||
div = form = null;
|
||||
|
||||
return isSupported;
|
||||
})()
|
||||
},
|
||||
|
||||
ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>',
|
||||
JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/,
|
||||
|
||||
JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/,
|
||||
|
||||
emptyFunction: function() { },
|
||||
K: function(x) { return x }
|
||||
};
|
||||
|
||||
if (Prototype.Browser.MobileSafari)
|
||||
Prototype.BrowserFeatures.SpecificElementExtensions = false;
|
||||
|
||||
var Abstract = { };
|
||||
|
||||
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"
|
||||
//= require "ajax"
|
||||
//= require "dom"
|
||||
|
||||
<%= include 'lang/class.js', 'lang/object.js', 'lang/function.js' %>
|
||||
|
||||
<%= include 'lang/date.js', 'lang/regexp.js', 'lang/periodical_executer.js' %>
|
||||
|
||||
<%= include 'lang/string.js', 'lang/template.js' %>
|
||||
|
||||
<%= include 'lang/enumerable.js', 'lang/array.js', 'lang/hash.js' %>
|
||||
|
||||
<%= include 'lang/number.js', 'lang/range.js' %>
|
||||
|
||||
<%= include 'ajax/ajax.js', 'ajax/responders.js', 'ajax/base.js', 'ajax/request.js', 'ajax/response.js' %>
|
||||
|
||||
<%= include 'ajax/updater.js', 'ajax/periodical_updater.js' %>
|
||||
|
||||
<%= include 'dom/dom.js', 'dom/selector.js', 'dom/form.js', 'dom/event.js' %>
|
||||
|
||||
<%= include 'deprecated.js' %>
|
||||
|
||||
Element.addMethods();
|
||||
//= require "deprecated"
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
Prototype._original_property = window.Sizzle;
|
||||
//= require "sizzle"
|
||||
|
||||
Prototype.Selector = (function(engine) {
|
||||
function extend(elements) {
|
||||
for (var i = 0, length = elements.length; i < length; i++) {
|
||||
Element.extend(elements[i]);
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
|
||||
function select(selector, scope) {
|
||||
return extend(engine(selector, scope || document));
|
||||
}
|
||||
|
||||
function match(element, selector) {
|
||||
return engine.matches(selector, [element]).length == 1;
|
||||
}
|
||||
|
||||
return {
|
||||
engine: engine,
|
||||
select: select,
|
||||
match: match
|
||||
};
|
||||
})(Sizzle);
|
||||
|
||||
// Restore globals.
|
||||
window.Sizzle = Prototype._original_property;
|
||||
delete Prototype._original_property;
|
File diff suppressed because it is too large
Load Diff
|
@ -225,14 +225,38 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<p id="mouseenter"><code>mouseenter</code> test</p>
|
||||
|
||||
<script type="text/javascript">
|
||||
$('mouseenter').observe('click', function() {
|
||||
$('mouseenter').insert("<br />carefully mouse over the word 'mouseenter', then outside of the box entirely");
|
||||
$('mouseenter').observe('mouseenter', function(event) {
|
||||
this.failed();
|
||||
$('mouseenter').stopObserving();
|
||||
});
|
||||
$('mouseenter').observe('mouseleave', function(event) {
|
||||
if ($(event.relatedTarget).descendantOf($('mouseenter'))) {
|
||||
$('mouseenter').failed();
|
||||
} else $('mouseenter').passed();
|
||||
|
||||
$('mouseenter').stopObserving();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<p id="addunload">Add unload events</p>
|
||||
|
||||
<script type="text/javascript">
|
||||
$('addunload').observe('click', function(e){
|
||||
if (this._done) return
|
||||
if (this._done) return;
|
||||
|
||||
window.onunload = function(){ alert('inline unload fired!') }
|
||||
Event.observe(window, 'unload', function(){ alert('observed unload fired!') })
|
||||
Event.observe(window, 'unload', function(event){
|
||||
if (!event.target) {
|
||||
alert('FAILURE: event.target should not be null!');
|
||||
}
|
||||
alert('observed unload fired!')
|
||||
});
|
||||
|
||||
this.update('Registered two unload events, one inline ("onunload") and one regular - try to refresh, both should fire')
|
||||
this._done = true
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
body, div, p, h1, h2, h3, ul, ol, span, a, table, td, form, img, li {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size:0.8em;
|
||||
}
|
||||
|
||||
#log {
|
||||
padding-bottom: 1em;
|
||||
border-bottom: 2px solid #000;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.logsummary {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
padding: 1ex;
|
||||
border: 1px solid #000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.logtable {
|
||||
width:100%;
|
||||
border-collapse: collapse;
|
||||
border: 1px dotted #666;
|
||||
}
|
||||
|
||||
.logtable td, .logtable th {
|
||||
text-align: left;
|
||||
padding: 3px 8px;
|
||||
border: 1px dotted #666;
|
||||
}
|
||||
|
||||
.logtable .passed {
|
||||
background-color: #cfc;
|
||||
}
|
||||
|
||||
.logtable .failed, .logtable .error {
|
||||
background-color: #fcc;
|
||||
}
|
||||
|
||||
.logtable td div.action_buttons {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.logtable td div.action_buttons input {
|
||||
margin: 0 5px;
|
||||
font-size: 10px;
|
||||
}
|
|
@ -1,562 +0,0 @@
|
|||
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
|
||||
// (c) 2005 Jon Tirsen (http://www.tirsen.com)
|
||||
// (c) 2005 Michael Schuerig (http://www.schuerig.de/michael/)
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining
|
||||
// a copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||
// permit persons to whom the Software is furnished to do so, subject to
|
||||
// the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
// experimental, Firefox-only
|
||||
Event.simulateMouse = function(element, eventName) {
|
||||
var options = Object.extend({
|
||||
pointerX: 0,
|
||||
pointerY: 0,
|
||||
buttons: 0
|
||||
}, arguments[2] || {});
|
||||
var oEvent = document.createEvent("MouseEvents");
|
||||
oEvent.initMouseEvent(eventName, true, true, document.defaultView,
|
||||
options.buttons, options.pointerX, options.pointerY, options.pointerX, options.pointerY,
|
||||
false, false, false, false, 0, $(element));
|
||||
|
||||
if(this.mark) Element.remove(this.mark);
|
||||
|
||||
var style = 'position: absolute; width: 5px; height: 5px;' +
|
||||
'top: #{pointerY}px; left: #{pointerX}px;'.interpolate(options) +
|
||||
'border-top: 1px solid red; border-left: 1px solid red;'
|
||||
|
||||
this.mark = new Element('div', { style: style });
|
||||
this.mark.appendChild(document.createTextNode(" "));
|
||||
document.body.appendChild(this.mark);
|
||||
|
||||
if(this.step)
|
||||
alert('['+new Date().getTime().toString()+'] '+eventName+'/'+Test.Unit.inspect(options));
|
||||
|
||||
$(element).dispatchEvent(oEvent);
|
||||
};
|
||||
|
||||
// Note: Due to a fix in Firefox 1.0.5/6 that probably fixed "too much", this doesn't work in 1.0.6 or DP2.
|
||||
// You need to downgrade to 1.0.4 for now to get this working
|
||||
// See https://bugzilla.mozilla.org/show_bug.cgi?id=289940 for the fix that fixed too much
|
||||
Event.simulateKey = function(element, eventName) {
|
||||
var options = Object.extend({
|
||||
ctrlKey: false,
|
||||
altKey: false,
|
||||
shiftKey: false,
|
||||
metaKey: false,
|
||||
keyCode: 0,
|
||||
charCode: 0
|
||||
}, arguments[2] || {});
|
||||
|
||||
var oEvent = document.createEvent("KeyEvents");
|
||||
oEvent.initKeyEvent(eventName, true, true, window,
|
||||
options.ctrlKey, options.altKey, options.shiftKey, options.metaKey,
|
||||
options.keyCode, options.charCode );
|
||||
$(element).dispatchEvent(oEvent);
|
||||
};
|
||||
|
||||
Event.simulateKeys = function(element, command) {
|
||||
for(var i=0; i<command.length; i++) {
|
||||
Event.simulateKey(element,'keypress',{charCode:command.charCodeAt(i)});
|
||||
}
|
||||
};
|
||||
|
||||
var Test = {
|
||||
Unit: {
|
||||
inspect: Object.inspect // security exception workaround
|
||||
}
|
||||
};
|
||||
|
||||
Test.Unit.Logger = Class.create({
|
||||
initialize: function(element) {
|
||||
this.element = $(element);
|
||||
if (this.element) this._createLogTable();
|
||||
this.tbody = $(this.element.getElementsByTagName('tbody')[0]);
|
||||
},
|
||||
|
||||
start: function(testName) {
|
||||
if (!this.element) return;
|
||||
this.tbody.insert('<tr><td>' + testName + '</td><td></td><td></td></tr>');
|
||||
},
|
||||
|
||||
setStatus: function(status) {
|
||||
this.getLastLogLine().addClassName(status);
|
||||
$(this.getLastLogLine().getElementsByTagName('td')[1]).update(status);
|
||||
},
|
||||
|
||||
finish: function(status, summary) {
|
||||
if (!this.element) return;
|
||||
this.setStatus(status);
|
||||
this.message(summary);
|
||||
},
|
||||
|
||||
message: function(message) {
|
||||
if (!this.element) return;
|
||||
this.getMessageCell().update(this._toHTML(message));
|
||||
},
|
||||
|
||||
summary: function(summary) {
|
||||
if (!this.element) return;
|
||||
var div = $(this.element.getElementsByTagName('div')[0]);
|
||||
div.update(this._toHTML(summary));
|
||||
},
|
||||
|
||||
getLastLogLine: function() {
|
||||
//return this.element.descendants('tr').last();
|
||||
var trs = this.element.getElementsByTagName('tr');
|
||||
return $(trs[trs.length - 1]);
|
||||
},
|
||||
|
||||
getMessageCell: function() {
|
||||
return this.getLastLogLine().down('td', 2);
|
||||
var tds = this.getLastLogLine().getElementsByTagName('td');
|
||||
return $(tds[2]);
|
||||
},
|
||||
|
||||
_createLogTable: function() {
|
||||
var html = '<div class="logsummary">running...</div>' +
|
||||
'<table class="logtable">' +
|
||||
'<thead><tr><th>Status</th><th>Test</th><th>Message</th></tr></thead>' +
|
||||
'<tbody class="loglines"></tbody>' +
|
||||
'</table>';
|
||||
this.element.update(html);
|
||||
|
||||
},
|
||||
|
||||
appendActionButtons: function(actions) {
|
||||
actions = $H(actions);
|
||||
if (!actions.any()) return;
|
||||
var div = new Element("div", {className: 'action_buttons'});
|
||||
actions.inject(div, function(container, action) {
|
||||
var button = new Element("input").setValue(action.key).observe("click", action.value);
|
||||
button.type = "button";
|
||||
return container.insert(button);
|
||||
});
|
||||
this.getMessageCell().insert(div);
|
||||
},
|
||||
|
||||
_toHTML: function(txt) {
|
||||
return txt.escapeHTML().replace(/\n/g,"<br />");
|
||||
}
|
||||
});
|
||||
|
||||
Test.Unit.Runner = Class.create({
|
||||
initialize: function(testcases) {
|
||||
var options = this.options = Object.extend({
|
||||
testLog: 'testlog'
|
||||
}, arguments[1] || {});
|
||||
|
||||
options.resultsURL = this.queryParams.resultsURL;
|
||||
|
||||
this.tests = this.getTests(testcases);
|
||||
this.currentTest = 0;
|
||||
|
||||
Event.observe(window, "load", function() {
|
||||
this.logger = new Test.Unit.Logger($(options.testLog));
|
||||
this.runTests.bind(this).delay(0.1);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
queryParams: window.location.search.parseQuery(),
|
||||
|
||||
getTests: function(testcases) {
|
||||
var tests, options = this.options;
|
||||
if (this.queryParams.tests) tests = this.queryParams.tests.split(',');
|
||||
else if (options.tests) tests = options.tests;
|
||||
else if (options.test) tests = [option.test];
|
||||
else tests = Object.keys(testcases).grep(/^test/);
|
||||
|
||||
return tests.map(function(test) {
|
||||
if (testcases[test])
|
||||
return new Test.Unit.Testcase(test, testcases[test], testcases.setup, testcases.teardown);
|
||||
}).compact();
|
||||
},
|
||||
|
||||
getResult: function() {
|
||||
var results = {
|
||||
tests: this.tests.length,
|
||||
assertions: 0,
|
||||
failures: 0,
|
||||
errors: 0
|
||||
};
|
||||
|
||||
return this.tests.inject(results, function(results, test) {
|
||||
results.assertions += test.assertions;
|
||||
results.failures += test.failures;
|
||||
results.errors += test.errors;
|
||||
return results;
|
||||
});
|
||||
},
|
||||
|
||||
postResults: function() {
|
||||
if (this.options.resultsURL) {
|
||||
new Ajax.Request(this.options.resultsURL,
|
||||
{ method: 'get', parameters: this.getResult(), asynchronous: false });
|
||||
}
|
||||
},
|
||||
|
||||
runTests: function() {
|
||||
var test = this.tests[this.currentTest], actions;
|
||||
|
||||
if (!test) return this.finish();
|
||||
if (!test.isWaiting) this.logger.start(test.name);
|
||||
test.run();
|
||||
if(test.isWaiting) {
|
||||
this.logger.message("Waiting for " + test.timeToWait + "ms");
|
||||
setTimeout(this.runTests.bind(this), test.timeToWait || 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.finish(test.status(), test.summary());
|
||||
if (actions = test.actions) this.logger.appendActionButtons(actions);
|
||||
this.currentTest++;
|
||||
// tail recursive, hopefully the browser will skip the stackframe
|
||||
this.runTests();
|
||||
},
|
||||
|
||||
finish: function() {
|
||||
this.postResults();
|
||||
this.logger.summary(this.summary());
|
||||
},
|
||||
|
||||
summary: function() {
|
||||
return '#{tests} tests, #{assertions} assertions, #{failures} failures, #{errors} errors'
|
||||
.interpolate(this.getResult());
|
||||
}
|
||||
});
|
||||
|
||||
Test.Unit.MessageTemplate = Class.create({
|
||||
initialize: function(string) {
|
||||
var parts = [];
|
||||
(string || '').scan(/(?=[^\\])\?|(?:\\\?|[^\?])+/, function(part) {
|
||||
parts.push(part[0]);
|
||||
});
|
||||
this.parts = parts;
|
||||
},
|
||||
|
||||
evaluate: function(params) {
|
||||
return this.parts.map(function(part) {
|
||||
return part == '?' ? Test.Unit.inspect(params.shift()) : part.replace(/\\\?/, '?');
|
||||
}).join('');
|
||||
}
|
||||
});
|
||||
|
||||
Test.Unit.Assertions = {
|
||||
buildMessage: function(message, template) {
|
||||
var args = $A(arguments).slice(2);
|
||||
return (message ? message + '\n' : '') + new Test.Unit.MessageTemplate(template).evaluate(args);
|
||||
},
|
||||
|
||||
flunk: function(message) {
|
||||
this.assertBlock(message || 'Flunked', function() { return false });
|
||||
},
|
||||
|
||||
assertBlock: function(message, block) {
|
||||
try {
|
||||
block.call(this) ? this.pass() : this.fail(message);
|
||||
} catch(e) { this.error(e) }
|
||||
},
|
||||
|
||||
assert: function(expression, message) {
|
||||
message = this.buildMessage(message || 'assert', 'got <?>', expression);
|
||||
this.assertBlock(message, function() { return expression });
|
||||
},
|
||||
|
||||
assertEqual: function(expected, actual, message) {
|
||||
message = this.buildMessage(message || 'assertEqual', 'expected <?>, actual: <?>', expected, actual);
|
||||
this.assertBlock(message, function() { return expected == actual });
|
||||
},
|
||||
|
||||
assertNotEqual: function(expected, actual, message) {
|
||||
message = this.buildMessage(message || 'assertNotEqual', 'expected <?>, actual: <?>', expected, actual);
|
||||
this.assertBlock(message, function() { return expected != actual });
|
||||
},
|
||||
|
||||
assertEnumEqual: function(expected, actual, message) {
|
||||
expected = $A(expected);
|
||||
actual = $A(actual);
|
||||
message = this.buildMessage(message || 'assertEnumEqual', 'expected <?>, actual: <?>', expected, actual);
|
||||
this.assertBlock(message, function() {
|
||||
return expected.length == actual.length && expected.zip(actual).all(function(pair) { return pair[0] == pair[1] });
|
||||
});
|
||||
},
|
||||
|
||||
assertEnumNotEqual: function(expected, actual, message) {
|
||||
expected = $A(expected);
|
||||
actual = $A(actual);
|
||||
message = this.buildMessage(message || 'assertEnumNotEqual', '<?> was the same as <?>', expected, actual);
|
||||
this.assertBlock(message, function() {
|
||||
return expected.length != actual.length || expected.zip(actual).any(function(pair) { return pair[0] != pair[1] });
|
||||
});
|
||||
},
|
||||
|
||||
assertHashEqual: function(expected, actual, message) {
|
||||
expected = $H(expected);
|
||||
actual = $H(actual);
|
||||
var expected_array = expected.toArray().sort(), actual_array = actual.toArray().sort();
|
||||
message = this.buildMessage(message || 'assertHashEqual', 'expected <?>, actual: <?>', expected, actual);
|
||||
// from now we recursively zip & compare nested arrays
|
||||
var block = function() {
|
||||
return expected_array.length == actual_array.length &&
|
||||
expected_array.zip(actual_array).all(function(pair) {
|
||||
return pair.all(Object.isArray) ?
|
||||
pair[0].zip(pair[1]).all(arguments.callee) : pair[0] == pair[1];
|
||||
});
|
||||
};
|
||||
this.assertBlock(message, block);
|
||||
},
|
||||
|
||||
assertHashNotEqual: function(expected, actual, message) {
|
||||
expected = $H(expected);
|
||||
actual = $H(actual);
|
||||
var expected_array = expected.toArray().sort(), actual_array = actual.toArray().sort();
|
||||
message = this.buildMessage(message || 'assertHashNotEqual', '<?> was the same as <?>', expected, actual);
|
||||
// from now we recursively zip & compare nested arrays
|
||||
var block = function() {
|
||||
return !(expected_array.length == actual_array.length &&
|
||||
expected_array.zip(actual_array).all(function(pair) {
|
||||
return pair.all(Object.isArray) ?
|
||||
pair[0].zip(pair[1]).all(arguments.callee) : pair[0] == pair[1];
|
||||
}));
|
||||
};
|
||||
this.assertBlock(message, block);
|
||||
},
|
||||
|
||||
assertIdentical: function(expected, actual, message) {
|
||||
message = this.buildMessage(message || 'assertIdentical', 'expected <?>, actual: <?>', expected, actual);
|
||||
this.assertBlock(message, function() { return expected === actual });
|
||||
},
|
||||
|
||||
assertNotIdentical: function(expected, actual, message) {
|
||||
message = this.buildMessage(message || 'assertNotIdentical', 'expected <?>, actual: <?>', expected, actual);
|
||||
this.assertBlock(message, function() { return expected !== actual });
|
||||
},
|
||||
|
||||
assertNull: function(obj, message) {
|
||||
message = this.buildMessage(message || 'assertNull', 'got <?>', obj);
|
||||
this.assertBlock(message, function() { return obj === null });
|
||||
},
|
||||
|
||||
assertNotNull: function(obj, message) {
|
||||
message = this.buildMessage(message || 'assertNotNull', 'got <?>', obj);
|
||||
this.assertBlock(message, function() { return obj !== null });
|
||||
},
|
||||
|
||||
assertUndefined: function(obj, message) {
|
||||
message = this.buildMessage(message || 'assertUndefined', 'got <?>', obj);
|
||||
this.assertBlock(message, function() { return typeof obj == "undefined" });
|
||||
},
|
||||
|
||||
assertNotUndefined: function(obj, message) {
|
||||
message = this.buildMessage(message || 'assertNotUndefined', 'got <?>', obj);
|
||||
this.assertBlock(message, function() { return typeof obj != "undefined" });
|
||||
},
|
||||
|
||||
assertNullOrUndefined: function(obj, message) {
|
||||
message = this.buildMessage(message || 'assertNullOrUndefined', 'got <?>', obj);
|
||||
this.assertBlock(message, function() { return obj == null });
|
||||
},
|
||||
|
||||
assertNotNullOrUndefined: function(obj, message) {
|
||||
message = this.buildMessage(message || 'assertNotNullOrUndefined', 'got <?>', obj);
|
||||
this.assertBlock(message, function() { return obj != null });
|
||||
},
|
||||
|
||||
assertMatch: function(expected, actual, message) {
|
||||
message = this.buildMessage(message || 'assertMatch', 'regex <?> did not match <?>', expected, actual);
|
||||
this.assertBlock(message, function() { return new RegExp(expected).exec(actual) });
|
||||
},
|
||||
|
||||
assertNoMatch: function(expected, actual, message) {
|
||||
message = this.buildMessage(message || 'assertNoMatch', 'regex <?> matched <?>', expected, actual);
|
||||
this.assertBlock(message, function() { return !(new RegExp(expected).exec(actual)) });
|
||||
},
|
||||
|
||||
assertHidden: function(element, message) {
|
||||
message = this.buildMessage(message || 'assertHidden', '? isn\'t hidden.', element);
|
||||
this.assertBlock(message, function() { return element.style.display == 'none' });
|
||||
},
|
||||
|
||||
assertInstanceOf: function(expected, actual, message) {
|
||||
message = this.buildMessage(message || 'assertInstanceOf', '<?> was not an instance of the expected type', actual);
|
||||
this.assertBlock(message, function() { return actual instanceof expected });
|
||||
},
|
||||
|
||||
assertNotInstanceOf: function(expected, actual, message) {
|
||||
message = this.buildMessage(message || 'assertNotInstanceOf', '<?> was an instance of the expected type', actual);
|
||||
this.assertBlock(message, function() { return !(actual instanceof expected) });
|
||||
},
|
||||
|
||||
assertRespondsTo: function(method, obj, message) {
|
||||
message = this.buildMessage(message || 'assertRespondsTo', 'object doesn\'t respond to <?>', method);
|
||||
this.assertBlock(message, function() { return (method in obj && typeof obj[method] == 'function') });
|
||||
},
|
||||
|
||||
assertRaise: function(exceptionName, method, message) {
|
||||
message = this.buildMessage(message || 'assertRaise', '<?> exception expected but none was raised', exceptionName);
|
||||
var block = function() {
|
||||
try {
|
||||
method();
|
||||
return false;
|
||||
} catch(e) {
|
||||
if (e.name == exceptionName) return true;
|
||||
else throw e;
|
||||
}
|
||||
};
|
||||
this.assertBlock(message, block);
|
||||
},
|
||||
|
||||
assertNothingRaised: function(method, message) {
|
||||
try {
|
||||
method();
|
||||
this.assert(true, "Expected nothing to be thrown");
|
||||
} catch(e) {
|
||||
message = this.buildMessage(message || 'assertNothingRaised', '<?> was thrown when nothing was expected.', e);
|
||||
this.flunk(message);
|
||||
}
|
||||
},
|
||||
|
||||
_isVisible: function(element) {
|
||||
element = $(element);
|
||||
if(!element.parentNode) return true;
|
||||
this.assertNotNull(element);
|
||||
if(element.style && Element.getStyle(element, 'display') == 'none')
|
||||
return false;
|
||||
|
||||
return arguments.callee.call(this, element.parentNode);
|
||||
},
|
||||
|
||||
assertVisible: function(element, message) {
|
||||
message = this.buildMessage(message, '? was not visible.', element);
|
||||
this.assertBlock(message, function() { return this._isVisible(element) });
|
||||
},
|
||||
|
||||
assertNotVisible: function(element, message) {
|
||||
message = this.buildMessage(message, '? was not hidden and didn\'t have a hidden parent either.', element);
|
||||
this.assertBlock(message, function() { return !this._isVisible(element) });
|
||||
},
|
||||
|
||||
assertElementsMatch: function() {
|
||||
var message, pass = true, expressions = $A(arguments), elements = $A(expressions.shift());
|
||||
if (elements.length != expressions.length) {
|
||||
message = this.buildMessage('assertElementsMatch', 'size mismatch: ? elements, ? expressions (?).', elements.length, expressions.length, expressions);
|
||||
this.flunk(message);
|
||||
pass = false;
|
||||
}
|
||||
elements.zip(expressions).all(function(pair, index) {
|
||||
var element = $(pair.first()), expression = pair.last();
|
||||
if (element.match(expression)) return true;
|
||||
message = this.buildMessage('assertElementsMatch', 'In index <?>: expected <?> but got ?', index, expression, element);
|
||||
this.flunk(message);
|
||||
pass = false;
|
||||
}.bind(this))
|
||||
|
||||
if (pass) this.assert(true, "Expected all elements to match.");
|
||||
},
|
||||
|
||||
assertElementMatches: function(element, expression, message) {
|
||||
this.assertElementsMatch([element], expression);
|
||||
}
|
||||
};
|
||||
|
||||
Test.Unit.Testcase = Class.create(Test.Unit.Assertions, {
|
||||
initialize: function(name, test, setup, teardown) {
|
||||
this.name = name;
|
||||
this.test = test || Prototype.emptyFunction;
|
||||
this.setup = setup || Prototype.emptyFunction;
|
||||
this.teardown = teardown || Prototype.emptyFunction;
|
||||
this.messages = [];
|
||||
this.actions = {};
|
||||
},
|
||||
|
||||
isWaiting: false,
|
||||
timeToWait: 1000,
|
||||
assertions: 0,
|
||||
failures: 0,
|
||||
errors: 0,
|
||||
isRunningFromRake: window.location.port == 4711,
|
||||
|
||||
wait: function(time, nextPart) {
|
||||
this.isWaiting = true;
|
||||
this.test = nextPart;
|
||||
this.timeToWait = time;
|
||||
},
|
||||
|
||||
run: function(rethrow) {
|
||||
try {
|
||||
try {
|
||||
if (!this.isWaiting) this.setup();
|
||||
this.isWaiting = false;
|
||||
this.test();
|
||||
} finally {
|
||||
if(!this.isWaiting) {
|
||||
this.teardown();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(e) {
|
||||
if (rethrow) throw e;
|
||||
this.error(e, this);
|
||||
}
|
||||
},
|
||||
|
||||
summary: function() {
|
||||
var msg = '#{assertions} assertions, #{failures} failures, #{errors} errors\n';
|
||||
return msg.interpolate(this) + this.messages.join("\n");
|
||||
},
|
||||
|
||||
pass: function() {
|
||||
this.assertions++;
|
||||
},
|
||||
|
||||
fail: function(message) {
|
||||
this.failures++;
|
||||
var line = "";
|
||||
try {
|
||||
throw new Error("stack");
|
||||
} catch(e){
|
||||
line = (/\.html:(\d+)/.exec(e.stack || '') || ['',''])[1];
|
||||
}
|
||||
this.messages.push("Failure: " + message + (line ? " Line #" + line : ""));
|
||||
},
|
||||
|
||||
info: function(message) {
|
||||
this.messages.push("Info: " + message);
|
||||
},
|
||||
|
||||
error: function(error, test) {
|
||||
this.errors++;
|
||||
this.actions['retry with throw'] = function() { test.run(true) };
|
||||
this.messages.push(error.name + ": "+ error.message + "(" + Test.Unit.inspect(error) + ")");
|
||||
},
|
||||
|
||||
status: function() {
|
||||
if (this.failures > 0) return 'failed';
|
||||
if (this.errors > 0) return 'error';
|
||||
return 'passed';
|
||||
},
|
||||
|
||||
benchmark: function(operation, iterations) {
|
||||
var startAt = new Date();
|
||||
(iterations || 1).times(operation);
|
||||
var timeTaken = ((new Date())-startAt);
|
||||
this.info((arguments[2] || 'Operation') + ' finished ' +
|
||||
iterations + ' iterations in ' + (timeTaken/1000)+'s' );
|
||||
return timeTaken;
|
||||
}
|
||||
});
|
|
@ -1,504 +0,0 @@
|
|||
require 'rake/tasklib'
|
||||
require 'thread'
|
||||
require 'webrick'
|
||||
require 'fileutils'
|
||||
include FileUtils
|
||||
require 'erb'
|
||||
|
||||
class Browser
|
||||
def supported?; true; end
|
||||
def setup ; end
|
||||
def open(url) ; end
|
||||
def teardown ; end
|
||||
|
||||
def host
|
||||
require 'rbconfig'
|
||||
Config::CONFIG['host']
|
||||
end
|
||||
|
||||
def macos?
|
||||
host.include?('darwin')
|
||||
end
|
||||
|
||||
def windows?
|
||||
host.include?('mswin')
|
||||
end
|
||||
|
||||
def linux?
|
||||
host.include?('linux')
|
||||
end
|
||||
|
||||
def applescript(script)
|
||||
raise "Can't run AppleScript on #{host}" unless macos?
|
||||
system "osascript -e '#{script}' 2>&1 >/dev/null"
|
||||
end
|
||||
end
|
||||
|
||||
class FirefoxBrowser < Browser
|
||||
def initialize(path=File.join(ENV['ProgramFiles'] || 'c:\Program Files', '\Mozilla Firefox\firefox.exe'))
|
||||
@path = path
|
||||
end
|
||||
|
||||
def visit(url)
|
||||
system("open -a Firefox '#{url}'") if macos?
|
||||
system("#{@path} #{url}") if windows?
|
||||
system("firefox #{url}") if linux?
|
||||
end
|
||||
|
||||
def to_s
|
||||
"Firefox"
|
||||
end
|
||||
end
|
||||
|
||||
class ChromeBrowser < Browser
|
||||
def initialize(path = nil)
|
||||
@path = path || File.join(
|
||||
ENV['UserPath'] || "C:/Documents and Settings/Administrator",
|
||||
"Local Settings",
|
||||
"Application Data",
|
||||
"Google",
|
||||
"Chrome",
|
||||
"Application",
|
||||
"chrome.exe"
|
||||
)
|
||||
end
|
||||
|
||||
def supported?
|
||||
windows?
|
||||
end
|
||||
|
||||
def visit(url)
|
||||
system("#{@path} #{url}")
|
||||
end
|
||||
|
||||
def to_s
|
||||
"Chrome"
|
||||
end
|
||||
end
|
||||
|
||||
class SafariBrowser < Browser
|
||||
def supported?
|
||||
macos?
|
||||
end
|
||||
|
||||
def setup
|
||||
applescript('tell application "Safari" to make new document')
|
||||
end
|
||||
|
||||
def visit(url)
|
||||
applescript('tell application "Safari" to set URL of front document to "' + url + '"')
|
||||
end
|
||||
|
||||
def teardown
|
||||
#applescript('tell application "Safari" to close front document')
|
||||
end
|
||||
|
||||
def to_s
|
||||
"Safari"
|
||||
end
|
||||
end
|
||||
|
||||
class IEBrowser < Browser
|
||||
def setup
|
||||
require 'win32ole' if windows?
|
||||
end
|
||||
|
||||
def supported?
|
||||
windows?
|
||||
end
|
||||
|
||||
def visit(url)
|
||||
if windows?
|
||||
ie = WIN32OLE.new('InternetExplorer.Application')
|
||||
ie.visible = true
|
||||
ie.Navigate(url)
|
||||
sleep 0.01 while ie.Busy || ie.ReadyState != 4
|
||||
end
|
||||
end
|
||||
|
||||
def to_s
|
||||
"Internet Explorer"
|
||||
end
|
||||
end
|
||||
|
||||
class KonquerorBrowser < Browser
|
||||
@@configDir = File.join((ENV['HOME'] || ''), '.kde', 'share', 'config')
|
||||
@@globalConfig = File.join(@@configDir, 'kdeglobals')
|
||||
@@konquerorConfig = File.join(@@configDir, 'konquerorrc')
|
||||
|
||||
def supported?
|
||||
linux?
|
||||
end
|
||||
|
||||
# Forces KDE's default browser to be Konqueror during the tests, and forces
|
||||
# Konqueror to open external URL requests in new tabs instead of a new
|
||||
# window.
|
||||
def setup
|
||||
cd @@configDir, :verbose => false do
|
||||
copy @@globalConfig, "#{@@globalConfig}.bak", :preserve => true, :verbose => false
|
||||
copy @@konquerorConfig, "#{@@konquerorConfig}.bak", :preserve => true, :verbose => false
|
||||
# Too lazy to write it in Ruby... Is sed dependency so bad?
|
||||
system "sed -ri /^BrowserApplication=/d '#{@@globalConfig}'"
|
||||
system "sed -ri /^KonquerorTabforExternalURL=/s:false:true: '#{@@konquerorConfig}'"
|
||||
end
|
||||
end
|
||||
|
||||
def teardown
|
||||
cd @@configDir, :verbose => false do
|
||||
copy "#{@@globalConfig}.bak", @@globalConfig, :preserve => true, :verbose => false
|
||||
copy "#{@@konquerorConfig}.bak", @@konquerorConfig, :preserve => true, :verbose => false
|
||||
end
|
||||
end
|
||||
|
||||
def visit(url)
|
||||
system("kfmclient openURL #{url}")
|
||||
end
|
||||
|
||||
def to_s
|
||||
"Konqueror"
|
||||
end
|
||||
end
|
||||
|
||||
class OperaBrowser < Browser
|
||||
def initialize(path='c:\Program Files\Opera\Opera.exe')
|
||||
@path = path
|
||||
end
|
||||
|
||||
def setup
|
||||
if windows?
|
||||
puts %{
|
||||
MAJOR ANNOYANCE on Windows.
|
||||
You have to shut down Opera manually after each test
|
||||
for the script to proceed.
|
||||
Any suggestions on fixing this is GREATLY appreciated!
|
||||
Thank you for your understanding.
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def visit(url)
|
||||
applescript('tell application "Opera" to GetURL "' + url + '"') if macos?
|
||||
system("#{@path} #{url}") if windows?
|
||||
system("opera #{url}") if linux?
|
||||
end
|
||||
|
||||
def to_s
|
||||
"Opera"
|
||||
end
|
||||
end
|
||||
|
||||
# shut up, webrick :-)
|
||||
class ::WEBrick::HTTPServer
|
||||
def access_log(config, req, res)
|
||||
# nop
|
||||
end
|
||||
end
|
||||
|
||||
class ::WEBrick::BasicLog
|
||||
def log(level, data)
|
||||
# nop
|
||||
end
|
||||
end
|
||||
|
||||
class WEBrick::HTTPResponse
|
||||
alias send send_response
|
||||
def send_response(socket)
|
||||
send(socket) unless fail_silently?
|
||||
end
|
||||
|
||||
def fail_silently?
|
||||
@fail_silently
|
||||
end
|
||||
|
||||
def fail_silently
|
||||
@fail_silently = true
|
||||
end
|
||||
end
|
||||
|
||||
class WEBrick::HTTPRequest
|
||||
def to_json
|
||||
headers = []
|
||||
each { |k, v| headers.push "#{k.inspect}: #{v.inspect}" }
|
||||
headers = "{" << headers.join(', ') << "}"
|
||||
%({ "headers": #{headers}, "body": #{body.inspect}, "method": #{request_method.inspect} })
|
||||
end
|
||||
end
|
||||
|
||||
class WEBrick::HTTPServlet::AbstractServlet
|
||||
def prevent_caching(res)
|
||||
res['ETag'] = nil
|
||||
res['Last-Modified'] = Time.now + 100**4
|
||||
res['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0'
|
||||
res['Pragma'] = 'no-cache'
|
||||
res['Expires'] = Time.now - 100**4
|
||||
end
|
||||
end
|
||||
|
||||
class BasicServlet < WEBrick::HTTPServlet::AbstractServlet
|
||||
def do_GET(req, res)
|
||||
prevent_caching(res)
|
||||
res['Content-Type'] = "text/plain"
|
||||
|
||||
req.query.each do |k, v|
|
||||
res[k] = v unless k == 'responseBody'
|
||||
end
|
||||
res.body = req.query["responseBody"]
|
||||
|
||||
raise WEBrick::HTTPStatus::OK
|
||||
end
|
||||
|
||||
def do_POST(req, res)
|
||||
do_GET(req, res)
|
||||
end
|
||||
end
|
||||
|
||||
class SlowServlet < BasicServlet
|
||||
def do_GET(req, res)
|
||||
sleep(2)
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
class DownServlet < BasicServlet
|
||||
def do_GET(req, res)
|
||||
res.fail_silently
|
||||
end
|
||||
end
|
||||
|
||||
class InspectionServlet < BasicServlet
|
||||
def do_GET(req, res)
|
||||
prevent_caching(res)
|
||||
res['Content-Type'] = "application/json"
|
||||
res.body = req.to_json
|
||||
raise WEBrick::HTTPStatus::OK
|
||||
end
|
||||
end
|
||||
|
||||
class NonCachingFileHandler < WEBrick::HTTPServlet::FileHandler
|
||||
def do_GET(req, res)
|
||||
super
|
||||
set_default_content_type(res, req.path)
|
||||
prevent_caching(res)
|
||||
end
|
||||
|
||||
def set_default_content_type(res, path)
|
||||
res['Content-Type'] = case path
|
||||
when /\.js$/ then 'text/javascript'
|
||||
when /\.html$/ then 'text/html'
|
||||
when /\.css$/ then 'text/css'
|
||||
else 'text/plain'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class JavaScriptTestTask < ::Rake::TaskLib
|
||||
|
||||
def initialize(name=:test)
|
||||
@name = name
|
||||
@tests = []
|
||||
@browsers = []
|
||||
|
||||
@queue = Queue.new
|
||||
|
||||
@server = WEBrick::HTTPServer.new(:Port => 4711) # TODO: make port configurable
|
||||
@server.mount_proc("/results") do |req, res|
|
||||
@queue.push(req)
|
||||
res.body = "OK"
|
||||
end
|
||||
@server.mount("/response", BasicServlet)
|
||||
@server.mount("/slow", SlowServlet)
|
||||
@server.mount("/down", DownServlet)
|
||||
@server.mount("/inspect", InspectionServlet)
|
||||
yield self if block_given?
|
||||
define
|
||||
end
|
||||
|
||||
def define
|
||||
task @name do
|
||||
trap("INT") { @server.shutdown; exit }
|
||||
t = Thread.new { @server.start }
|
||||
|
||||
# run all combinations of browsers and tests
|
||||
@browsers.each do |browser|
|
||||
if browser.supported?
|
||||
t0 = Time.now
|
||||
test_suite_results = TestSuiteResults.new
|
||||
|
||||
browser.setup
|
||||
puts "\nStarted tests in #{browser}."
|
||||
|
||||
@tests.each do |test|
|
||||
browser.visit(get_url(test))
|
||||
results = TestResults.new(@queue.pop.query, test[:url])
|
||||
print results
|
||||
test_suite_results << results
|
||||
end
|
||||
|
||||
print "\nFinished in #{Time.now - t0} seconds."
|
||||
print test_suite_results
|
||||
browser.teardown
|
||||
else
|
||||
puts "\nSkipping #{browser}, not supported on this OS."
|
||||
end
|
||||
end
|
||||
|
||||
@server.shutdown
|
||||
t.join
|
||||
end
|
||||
end
|
||||
|
||||
def get_url(test)
|
||||
params = "resultsURL=http://localhost:4711/results&t=" + ("%.6f" % Time.now.to_f)
|
||||
params << "&tests=#{test[:testcases]}" unless test[:testcases] == :all
|
||||
"http://localhost:4711#{test[:url]}?#{params}"
|
||||
end
|
||||
|
||||
def mount(path, dir=nil)
|
||||
dir = Dir.pwd + path unless dir
|
||||
|
||||
# don't cache anything in our tests
|
||||
@server.mount(path, NonCachingFileHandler, dir)
|
||||
end
|
||||
|
||||
# test should be specified as a hash of the form
|
||||
# {:url => "url", :testcases => "testFoo,testBar"}.
|
||||
# specifying :testcases is optional
|
||||
def run(url, testcases = :all)
|
||||
@tests << { :url => url, :testcases => testcases }
|
||||
end
|
||||
|
||||
def browser(browser)
|
||||
browser =
|
||||
case(browser)
|
||||
when :firefox
|
||||
FirefoxBrowser.new
|
||||
when :safari
|
||||
SafariBrowser.new
|
||||
when :ie
|
||||
IEBrowser.new
|
||||
when :konqueror
|
||||
KonquerorBrowser.new
|
||||
when :opera
|
||||
OperaBrowser.new
|
||||
when :chrome
|
||||
ChromeBrowser.new
|
||||
else
|
||||
browser
|
||||
end
|
||||
|
||||
@browsers<<browser
|
||||
end
|
||||
end
|
||||
|
||||
class TestResults
|
||||
attr_reader :tests, :assertions, :failures, :errors, :filename
|
||||
def initialize(query, filename)
|
||||
@tests = query['tests'].to_i
|
||||
@assertions = query['assertions'].to_i
|
||||
@failures = query['failures'].to_i
|
||||
@errors = query['errors'].to_i
|
||||
@filename = filename
|
||||
end
|
||||
|
||||
def error?
|
||||
@errors > 0
|
||||
end
|
||||
|
||||
def failure?
|
||||
@failures > 0
|
||||
end
|
||||
|
||||
def to_s
|
||||
return "E" if error?
|
||||
return "F" if failure?
|
||||
"."
|
||||
end
|
||||
end
|
||||
|
||||
class TestSuiteResults
|
||||
def initialize
|
||||
@tests = 0
|
||||
@assertions = 0
|
||||
@failures = 0
|
||||
@errors = 0
|
||||
@error_files = []
|
||||
@failure_files = []
|
||||
end
|
||||
|
||||
def <<(result)
|
||||
@tests += result.tests
|
||||
@assertions += result.assertions
|
||||
@failures += result.failures
|
||||
@errors += result.errors
|
||||
@error_files.push(result.filename) if result.error?
|
||||
@failure_files.push(result.filename) if result.failure?
|
||||
end
|
||||
|
||||
def error?
|
||||
@errors > 0
|
||||
end
|
||||
|
||||
def failure?
|
||||
@failures > 0
|
||||
end
|
||||
|
||||
def to_s
|
||||
str = ""
|
||||
str << "\n Failures: #{@failure_files.join(', ')}" if failure?
|
||||
str << "\n Errors: #{@error_files.join(', ')}" if error?
|
||||
"#{str}\n#{summary}\n\n"
|
||||
end
|
||||
|
||||
def summary
|
||||
"#{@tests} tests, #{@assertions} assertions, #{@failures} failures, #{@errors} errors."
|
||||
end
|
||||
end
|
||||
|
||||
class PageBuilder
|
||||
UNITTEST_DIR = File.expand_path('test')
|
||||
FIXTURES_DIR = File.join(UNITTEST_DIR, 'unit', 'fixtures')
|
||||
TMP_DIR = File.join(UNITTEST_DIR, 'unit', 'tmp')
|
||||
TEMPLATES_DIR = File.join(UNITTEST_DIR, 'lib', 'templates')
|
||||
|
||||
def initialize(filename, template = 'default.erb')
|
||||
@filename = filename
|
||||
@template = File.join(self.class::TEMPLATES_DIR, template)
|
||||
@js_filename = File.basename(@filename)
|
||||
@basename = @js_filename.sub('_test.js', '')
|
||||
end
|
||||
|
||||
def html_fixtures
|
||||
content = ""
|
||||
file = File.join(FIXTURES_DIR, "#{@basename}.html")
|
||||
File.open(file).each { |l| content << l } if File.exists?(file)
|
||||
content
|
||||
end
|
||||
|
||||
def external_fixtures(extension)
|
||||
filename = "#{@basename}.#{extension}"
|
||||
File.exists?(File.join(FIXTURES_DIR, filename)) ? filename : nil
|
||||
end
|
||||
|
||||
def render
|
||||
@title = @basename.gsub('_', ' ').strip.capitalize
|
||||
@html_fixtures = html_fixtures
|
||||
@js_fixtures_filename = external_fixtures('js')
|
||||
@css_fixtures_filename = external_fixtures('css')
|
||||
|
||||
File.open(destination, 'w+') do |file|
|
||||
file << ERB.new(IO.read(@template), nil, '%').result(binding)
|
||||
end
|
||||
end
|
||||
|
||||
def destination
|
||||
name_file(:ext => 'html')
|
||||
end
|
||||
|
||||
def name_file(options = {})
|
||||
prefix = options[:prefix] ? "#{options[:prefix]}_" : ""
|
||||
suffix = options[:suffix] ? "_#{options[:suffix]}" : ""
|
||||
ext = options[:ext] ? options[:ext] : "js"
|
||||
filename = File.basename(@filename, '.js')
|
||||
File.join(TMP_DIR, "#{prefix}#{filename}#{suffix}.#{ext}")
|
||||
end
|
||||
end
|
|
@ -1,35 +0,0 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<title>Unit test file | <%= @title %></title>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||
<script src="../../../dist/prototype.js" type="text/javascript"></script>
|
||||
<script src="../../lib/assets/unittest.js" type="text/javascript"></script>
|
||||
<link rel="stylesheet" href="../../lib/assets/test.css" type="text/css" />
|
||||
|
||||
<% if @css_fixtures_filename %>
|
||||
<link rel="stylesheet" href="../fixtures/<%= @css_fixtures_filename %>" type="text/css" charset="utf-8" />
|
||||
<% end %>
|
||||
|
||||
<% if @js_fixtures_filename %>
|
||||
<script src="../fixtures/<%= @js_fixtures_filename %>" type="text/javascript" charset="utf-8"></script>
|
||||
<% end %>
|
||||
|
||||
<script src="../<%= @js_filename %>" type="text/javascript"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Unit test file</h1>
|
||||
<h2><%= @title %></h2>
|
||||
|
||||
<!-- This file is programmatically generated. Do not attempt to modify it. Instead, modify <%= @fixtures_filename %> -->
|
||||
|
||||
<!-- Log output start -->
|
||||
<div id="testlog"></div>
|
||||
<!-- Log output end -->
|
||||
|
||||
<!-- HTML Fixtures start -->
|
||||
<%= @html_fixtures %>
|
||||
<!-- HTML Fixtures end -->
|
||||
</body>
|
||||
</html>
|
|
@ -1,42 +0,0 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<title>Prototype Unit test file | <%= @title %></title>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
var eventResults = {};
|
||||
var originalElement = window.Element;
|
||||
</script>
|
||||
<script src="../../../dist/prototype.js" type="text/javascript"></script>
|
||||
<script src="../../lib/assets/unittest.js" type="text/javascript"></script>
|
||||
<link rel="stylesheet" href="../../lib/assets/test.css" type="text/css" />
|
||||
|
||||
<% if @css_fixtures_filename %>
|
||||
<link rel="stylesheet" href="../fixtures/<%= @css_fixtures_filename %>" type="text/css" charset="utf-8" />
|
||||
<% end %>
|
||||
|
||||
<% if @js_fixtures_filename %>
|
||||
<script src="../fixtures/<%= @js_fixtures_filename %>" type="text/javascript" charset="utf-8"></script>
|
||||
<% end %>
|
||||
|
||||
<script src="../<%= @js_filename %>" type="text/javascript"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Prototype Unit test file</h1>
|
||||
<h2><%= @title %></h2>
|
||||
|
||||
<!-- This file is programmatically generated. Do not attempt to modify it. Instead, modify <%= @fixtures_filename %> -->
|
||||
|
||||
<!-- Log output start -->
|
||||
<div id="testlog"></div>
|
||||
<!-- Log output end -->
|
||||
|
||||
<!-- HTML Fixtures start -->
|
||||
<%= @html_fixtures %>
|
||||
<!-- HTML Fixtures end -->
|
||||
</body>
|
||||
</html>
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
eventResults.endOfDocument = true;
|
||||
</script>
|
|
@ -96,7 +96,7 @@ new Test.Unit.Runner({
|
|||
onComplete: Prototype.emptyFunction
|
||||
}
|
||||
var request = new Ajax.Updater("content", "../fixtures/hello.js", options);
|
||||
request.options.onComplete = function() {};
|
||||
request.options.onComplete = Prototype.emptyFunction;
|
||||
this.assertIdentical(Prototype.emptyFunction, options.onComplete);
|
||||
},
|
||||
|
||||
|
@ -105,7 +105,7 @@ new Test.Unit.Runner({
|
|||
this.assertEqual(1, Ajax.Responders.responders.length);
|
||||
|
||||
var dummyResponder = {
|
||||
onComplete: function(req) { /* dummy */ }
|
||||
onComplete: Prototype.emptyFunction
|
||||
};
|
||||
|
||||
Ajax.Responders.register(dummyResponder);
|
||||
|
@ -376,4 +376,4 @@ new Test.Unit.Runner({
|
|||
this.info(message);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -33,6 +33,16 @@ new Test.Unit.Runner({
|
|||
this.assertEqual(3, $A(element.childNodes).length);
|
||||
},
|
||||
|
||||
testToArrayOnPrimitive: function() {
|
||||
this.assertEnumEqual(['a', 'b', 'c'], $A('abc'));
|
||||
this.assertEnumEqual([], $A(''));
|
||||
this.assertEnumEqual([], $A(null));
|
||||
this.assertEnumEqual([], $A(undefined));
|
||||
this.assertEnumEqual([], $A());
|
||||
this.assertEnumEqual([], $A(5));
|
||||
this.assertEnumEqual([], $A(true));
|
||||
},
|
||||
|
||||
testClear: function(){
|
||||
this.assertEnumEqual([], [].clear());
|
||||
this.assertEnumEqual([], [1].clear());
|
||||
|
@ -137,14 +147,6 @@ new Test.Unit.Runner({
|
|||
this.assertEqual('[\"a\", 1]', ['a', 1].toJSON());
|
||||
this.assertEqual('[\"a\", {\"b\": null}]', ['a', {'b': null}].toJSON());
|
||||
},
|
||||
|
||||
testReduce: function(){
|
||||
this.assertUndefined([].reduce());
|
||||
this.assertNull([null].reduce());
|
||||
this.assertEqual(1, [1].reduce());
|
||||
this.assertEnumEqual([1,2,3], [1,2,3].reduce());
|
||||
this.assertEnumEqual([1,null,3], [1,null,3].reduce());
|
||||
},
|
||||
|
||||
testReverse: function(){
|
||||
this.assertEnumEqual([], [].reverse());
|
||||
|
@ -185,5 +187,10 @@ new Test.Unit.Runner({
|
|||
this.assertEnumEqual(['a'], $w('a '));
|
||||
this.assertEnumEqual(['a'], $w(' a'));
|
||||
this.assertEnumEqual(['a', 'b', 'c', 'd'], $w(' a b\nc\t\nd\n'));
|
||||
},
|
||||
|
||||
testConcat: function(){
|
||||
var args = (function() { return [].concat(arguments) })(1, 2);
|
||||
this.assertIdentical(1, args[0][0]);
|
||||
}
|
||||
});
|
|
@ -1,354 +1,4 @@
|
|||
new Test.Unit.Runner({
|
||||
testFunctionArgumentNames: function() {
|
||||
this.assertEnumEqual([], (function() {}).argumentNames());
|
||||
this.assertEnumEqual(["one"], (function(one) {}).argumentNames());
|
||||
this.assertEnumEqual(["one", "two", "three"], (function(one, two, three) {}).argumentNames());
|
||||
this.assertEnumEqual(["one", "two", "three"], (function( one , two
|
||||
, three ) {}).argumentNames());
|
||||
this.assertEqual("$super", (function($super) {}).argumentNames().first());
|
||||
|
||||
function named1() {};
|
||||
this.assertEnumEqual([], named1.argumentNames());
|
||||
function named2(one) {};
|
||||
this.assertEnumEqual(["one"], named2.argumentNames());
|
||||
function named3(one, two, three) {};
|
||||
this.assertEnumEqual(["one", "two", "three"], named3.argumentNames());
|
||||
},
|
||||
|
||||
testFunctionBind: function() {
|
||||
function methodWithoutArguments() { return this.hi };
|
||||
function methodWithArguments() { return this.hi + ',' + $A(arguments).join(',') };
|
||||
var func = Prototype.emptyFunction;
|
||||
|
||||
this.assertIdentical(func, func.bind());
|
||||
this.assertIdentical(func, func.bind(undefined));
|
||||
this.assertNotIdentical(func, func.bind(null));
|
||||
|
||||
this.assertEqual('without', methodWithoutArguments.bind({ hi: 'without' })());
|
||||
this.assertEqual('with,arg1,arg2', methodWithArguments.bind({ hi: 'with' })('arg1','arg2'));
|
||||
this.assertEqual('withBindArgs,arg1,arg2',
|
||||
methodWithArguments.bind({ hi: 'withBindArgs' }, 'arg1', 'arg2')());
|
||||
this.assertEqual('withBindArgsAndArgs,arg1,arg2,arg3,arg4',
|
||||
methodWithArguments.bind({ hi: 'withBindArgsAndArgs' }, 'arg1', 'arg2')('arg3', 'arg4'));
|
||||
},
|
||||
|
||||
testFunctionCurry: function() {
|
||||
var split = function(delimiter, string) { return string.split(delimiter); };
|
||||
var splitOnColons = split.curry(":");
|
||||
this.assertNotIdentical(split, splitOnColons);
|
||||
this.assertEnumEqual(split(":", "0:1:2:3:4:5"), splitOnColons("0:1:2:3:4:5"));
|
||||
this.assertIdentical(split, split.curry());
|
||||
},
|
||||
|
||||
testFunctionDelay: function() {
|
||||
window.delayed = undefined;
|
||||
var delayedFunction = function() { window.delayed = true; };
|
||||
var delayedFunctionWithArgs = function() { window.delayedWithArgs = $A(arguments).join(' '); };
|
||||
delayedFunction.delay(0.8);
|
||||
delayedFunctionWithArgs.delay(0.8, 'hello', 'world');
|
||||
this.assertUndefined(window.delayed);
|
||||
this.wait(1000, function() {
|
||||
this.assert(window.delayed);
|
||||
this.assertEqual('hello world', window.delayedWithArgs);
|
||||
});
|
||||
},
|
||||
|
||||
testFunctionWrap: function() {
|
||||
function sayHello(){
|
||||
return 'hello world';
|
||||
};
|
||||
|
||||
this.assertEqual('HELLO WORLD', sayHello.wrap(function(proceed) {
|
||||
return proceed().toUpperCase();
|
||||
})());
|
||||
|
||||
var temp = String.prototype.capitalize;
|
||||
String.prototype.capitalize = String.prototype.capitalize.wrap(function(proceed, eachWord) {
|
||||
if(eachWord && this.include(' ')) return this.split(' ').map(function(str){
|
||||
return str.capitalize();
|
||||
}).join(' ');
|
||||
return proceed();
|
||||
});
|
||||
this.assertEqual('Hello world', 'hello world'.capitalize());
|
||||
this.assertEqual('Hello World', 'hello world'.capitalize(true));
|
||||
this.assertEqual('Hello', 'hello'.capitalize());
|
||||
String.prototype.capitalize = temp;
|
||||
},
|
||||
|
||||
testFunctionDefer: function() {
|
||||
window.deferred = undefined;
|
||||
var deferredFunction = function() { window.deferred = true; };
|
||||
deferredFunction.defer();
|
||||
this.assertUndefined(window.deferred);
|
||||
this.wait(50, function() {
|
||||
this.assert(window.deferred);
|
||||
|
||||
window.deferredValue = 0;
|
||||
var deferredFunction2 = function(arg) { window.deferredValue = arg; };
|
||||
deferredFunction2.defer('test');
|
||||
this.wait(50, function() {
|
||||
this.assertEqual('test', window.deferredValue);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
testFunctionMethodize: function() {
|
||||
var Foo = { bar: function(baz) { return baz } };
|
||||
var baz = { quux: Foo.bar.methodize() };
|
||||
|
||||
this.assertEqual(Foo.bar.methodize(), baz.quux);
|
||||
this.assertEqual(baz, Foo.bar(baz));
|
||||
this.assertEqual(baz, baz.quux());
|
||||
},
|
||||
|
||||
testObjectExtend: function() {
|
||||
var object = {foo: 'foo', bar: [1, 2, 3]};
|
||||
this.assertIdentical(object, Object.extend(object));
|
||||
this.assertHashEqual({foo: 'foo', bar: [1, 2, 3]}, object);
|
||||
this.assertIdentical(object, Object.extend(object, {bla: 123}));
|
||||
this.assertHashEqual({foo: 'foo', bar: [1, 2, 3], bla: 123}, object);
|
||||
this.assertHashEqual({foo: 'foo', bar: [1, 2, 3], bla: null},
|
||||
Object.extend(object, {bla: null}));
|
||||
},
|
||||
|
||||
testObjectToQueryString: function() {
|
||||
this.assertEqual('a=A&b=B&c=C&d=D%23', Object.toQueryString({a: 'A', b: 'B', c: 'C', d: 'D#'}));
|
||||
},
|
||||
|
||||
testObjectClone: function() {
|
||||
var object = {foo: 'foo', bar: [1, 2, 3]};
|
||||
this.assertNotIdentical(object, Object.clone(object));
|
||||
this.assertHashEqual(object, Object.clone(object));
|
||||
this.assertHashEqual({}, Object.clone());
|
||||
var clone = Object.clone(object);
|
||||
delete clone.bar;
|
||||
this.assertHashEqual({foo: 'foo'}, clone,
|
||||
"Optimizing Object.clone perf using prototyping doesn't allow properties to be deleted.");
|
||||
},
|
||||
|
||||
testObjectInspect: function() {
|
||||
this.assertEqual('undefined', Object.inspect());
|
||||
this.assertEqual('undefined', Object.inspect(undefined));
|
||||
this.assertEqual('null', Object.inspect(null));
|
||||
this.assertEqual("'foo\\\\b\\\'ar'", Object.inspect('foo\\b\'ar'));
|
||||
this.assertEqual('[]', Object.inspect([]));
|
||||
this.assertNothingRaised(function() { Object.inspect(window.Node) });
|
||||
},
|
||||
|
||||
testObjectToJSON: function() {
|
||||
this.assertUndefined(Object.toJSON(undefined));
|
||||
this.assertUndefined(Object.toJSON(Prototype.K));
|
||||
this.assertEqual('\"\"', Object.toJSON(''));
|
||||
this.assertEqual('[]', Object.toJSON([]));
|
||||
this.assertEqual('[\"a\"]', Object.toJSON(['a']));
|
||||
this.assertEqual('[\"a\", 1]', Object.toJSON(['a', 1]));
|
||||
this.assertEqual('[\"a\", {\"b\": null}]', Object.toJSON(['a', {'b': null}]));
|
||||
this.assertEqual('{\"a\": \"hello!\"}', Object.toJSON({a: 'hello!'}));
|
||||
this.assertEqual('{}', Object.toJSON({}));
|
||||
this.assertEqual('{}', Object.toJSON({a: undefined, b: undefined, c: Prototype.K}));
|
||||
this.assertEqual('{\"b\": [false, true], \"c\": {\"a\": \"hello!\"}}',
|
||||
Object.toJSON({'b': [undefined, false, true, undefined], c: {a: 'hello!'}}));
|
||||
this.assertEqual('{\"b\": [false, true], \"c\": {\"a\": \"hello!\"}}',
|
||||
Object.toJSON($H({'b': [undefined, false, true, undefined], c: {a: 'hello!'}})));
|
||||
this.assertEqual('true', Object.toJSON(true));
|
||||
this.assertEqual('false', Object.toJSON(false));
|
||||
this.assertEqual('null', Object.toJSON(null));
|
||||
var sam = new Person('sam');
|
||||
this.assertEqual('-sam', Object.toJSON(sam));
|
||||
this.assertEqual('-sam', sam.toJSON());
|
||||
var element = $('test');
|
||||
this.assertUndefined(Object.toJSON(element));
|
||||
element.toJSON = function(){return 'I\'m a div with id test'};
|
||||
this.assertEqual('I\'m a div with id test', Object.toJSON(element));
|
||||
},
|
||||
|
||||
testObjectToHTML: function() {
|
||||
this.assertIdentical('', Object.toHTML());
|
||||
this.assertIdentical('', Object.toHTML(''));
|
||||
this.assertIdentical('', Object.toHTML(null));
|
||||
this.assertIdentical('0', Object.toHTML(0));
|
||||
this.assertIdentical('123', Object.toHTML(123));
|
||||
this.assertEqual('hello world', Object.toHTML('hello world'));
|
||||
this.assertEqual('hello world', Object.toHTML({toHTML: function() { return 'hello world' }}));
|
||||
},
|
||||
|
||||
testObjectIsArray: function() {
|
||||
this.assert(Object.isArray([]));
|
||||
this.assert(Object.isArray([0]));
|
||||
this.assert(Object.isArray([0, 1]));
|
||||
this.assert(!Object.isArray({}));
|
||||
this.assert(!Object.isArray($('list').childNodes));
|
||||
this.assert(!Object.isArray());
|
||||
this.assert(!Object.isArray(''));
|
||||
this.assert(!Object.isArray('foo'));
|
||||
this.assert(!Object.isArray(0));
|
||||
this.assert(!Object.isArray(1));
|
||||
this.assert(!Object.isArray(null));
|
||||
this.assert(!Object.isArray(true));
|
||||
this.assert(!Object.isArray(false));
|
||||
this.assert(!Object.isArray(undefined));
|
||||
},
|
||||
|
||||
testObjectIsHash: function() {
|
||||
this.assert(Object.isHash($H()));
|
||||
this.assert(Object.isHash(new Hash()));
|
||||
this.assert(!Object.isHash({}));
|
||||
this.assert(!Object.isHash(null));
|
||||
this.assert(!Object.isHash());
|
||||
this.assert(!Object.isHash(''));
|
||||
this.assert(!Object.isHash(2));
|
||||
this.assert(!Object.isHash(false));
|
||||
this.assert(!Object.isHash(true));
|
||||
this.assert(!Object.isHash([]));
|
||||
},
|
||||
|
||||
testObjectIsElement: function() {
|
||||
this.assert(Object.isElement(document.createElement('div')));
|
||||
this.assert(Object.isElement(new Element('div')));
|
||||
this.assert(Object.isElement($('testlog')));
|
||||
this.assert(!Object.isElement(document.createTextNode('bla')));
|
||||
|
||||
// falsy variables should not mess up return value type
|
||||
this.assertIdentical(false, Object.isElement(0));
|
||||
this.assertIdentical(false, Object.isElement(''));
|
||||
this.assertIdentical(false, Object.isElement(NaN));
|
||||
this.assertIdentical(false, Object.isElement(null));
|
||||
this.assertIdentical(false, Object.isElement(undefined));
|
||||
},
|
||||
|
||||
testObjectIsFunction: function() {
|
||||
this.assert(Object.isFunction(function() { }));
|
||||
this.assert(Object.isFunction(Class.create()));
|
||||
this.assert(!Object.isFunction("a string"));
|
||||
this.assert(!Object.isFunction($("testlog")));
|
||||
this.assert(!Object.isFunction([]));
|
||||
this.assert(!Object.isFunction({}));
|
||||
this.assert(!Object.isFunction(0));
|
||||
this.assert(!Object.isFunction(false));
|
||||
this.assert(!Object.isFunction(undefined));
|
||||
},
|
||||
|
||||
testObjectIsString: function() {
|
||||
this.assert(!Object.isString(function() { }));
|
||||
this.assert(Object.isString("a string"));
|
||||
this.assert(!Object.isString(0));
|
||||
this.assert(!Object.isString([]));
|
||||
this.assert(!Object.isString({}));
|
||||
this.assert(!Object.isString(false));
|
||||
this.assert(!Object.isString(undefined));
|
||||
},
|
||||
|
||||
testObjectIsNumber: function() {
|
||||
this.assert(Object.isNumber(0));
|
||||
this.assert(Object.isNumber(1.0));
|
||||
this.assert(!Object.isNumber(function() { }));
|
||||
this.assert(!Object.isNumber("a string"));
|
||||
this.assert(!Object.isNumber([]));
|
||||
this.assert(!Object.isNumber({}));
|
||||
this.assert(!Object.isNumber(false));
|
||||
this.assert(!Object.isNumber(undefined));
|
||||
},
|
||||
|
||||
testObjectIsUndefined: function() {
|
||||
this.assert(Object.isUndefined(undefined));
|
||||
this.assert(!Object.isUndefined(null));
|
||||
this.assert(!Object.isUndefined(false));
|
||||
this.assert(!Object.isUndefined(0));
|
||||
this.assert(!Object.isUndefined(""));
|
||||
this.assert(!Object.isUndefined(function() { }));
|
||||
this.assert(!Object.isUndefined([]));
|
||||
this.assert(!Object.isUndefined({}));
|
||||
},
|
||||
|
||||
// sanity check
|
||||
testDoesntExtendObjectPrototype: function() {
|
||||
// for-in is supported with objects
|
||||
var iterations = 0, obj = { a: 1, b: 2, c: 3 };
|
||||
for(property in obj) iterations++;
|
||||
this.assertEqual(3, iterations);
|
||||
|
||||
// for-in is not supported with arrays
|
||||
iterations = 0;
|
||||
var arr = [1,2,3];
|
||||
for(property in arr) iterations++;
|
||||
this.assert(iterations > 3);
|
||||
},
|
||||
|
||||
testPeriodicalExecuterStop: function() {
|
||||
var peEventCount = 0;
|
||||
function peEventFired(pe) {
|
||||
if (++peEventCount > 2) pe.stop();
|
||||
}
|
||||
|
||||
// peEventFired will stop the PeriodicalExecuter after 3 callbacks
|
||||
new PeriodicalExecuter(peEventFired, 0.05);
|
||||
|
||||
this.wait(600, function() {
|
||||
this.assertEqual(3, peEventCount);
|
||||
});
|
||||
},
|
||||
|
||||
testBindAsEventListener: 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,
|
||||
this.assertEqual.bind(this, eventTest),
|
||||
this.assertEqual.bind(this, arg1),
|
||||
this.assertEqual.bind(this, arg2),
|
||||
this.assertEqual.bind(this, arg3), arg1, arg2, arg3 );
|
||||
call(eventTest);
|
||||
}
|
||||
},
|
||||
|
||||
testDateToJSON: function() {
|
||||
this.assertEqual('\"1970-01-01T00:00:00Z\"', new Date(Date.UTC(1970, 0, 1)).toJSON());
|
||||
},
|
||||
|
||||
testRegExpEscape: function() {
|
||||
this.assertEqual('word', RegExp.escape('word'));
|
||||
this.assertEqual('\\/slashes\\/', RegExp.escape('/slashes/'));
|
||||
this.assertEqual('\\\\backslashes\\\\', RegExp.escape('\\backslashes\\'));
|
||||
this.assertEqual('\\\\border of word', RegExp.escape('\\border of word'));
|
||||
|
||||
this.assertEqual('\\(\\?\\:non-capturing\\)', RegExp.escape('(?:non-capturing)'));
|
||||
this.assertEqual('non-capturing', new RegExp(RegExp.escape('(?:') + '([^)]+)').exec('(?:non-capturing)')[1]);
|
||||
|
||||
this.assertEqual('\\(\\?\\=positive-lookahead\\)', RegExp.escape('(?=positive-lookahead)'));
|
||||
this.assertEqual('positive-lookahead', new RegExp(RegExp.escape('(?=') + '([^)]+)').exec('(?=positive-lookahead)')[1]);
|
||||
|
||||
this.assertEqual('\\(\\?<\\=positive-lookbehind\\)', RegExp.escape('(?<=positive-lookbehind)'));
|
||||
this.assertEqual('positive-lookbehind', new RegExp(RegExp.escape('(?<=') + '([^)]+)').exec('(?<=positive-lookbehind)')[1]);
|
||||
|
||||
this.assertEqual('\\(\\?\\!negative-lookahead\\)', RegExp.escape('(?!negative-lookahead)'));
|
||||
this.assertEqual('negative-lookahead', new RegExp(RegExp.escape('(?!') + '([^)]+)').exec('(?!negative-lookahead)')[1]);
|
||||
|
||||
this.assertEqual('\\(\\?<\\!negative-lookbehind\\)', RegExp.escape('(?<!negative-lookbehind)'));
|
||||
this.assertEqual('negative-lookbehind', new RegExp(RegExp.escape('(?<!') + '([^)]+)').exec('(?<!negative-lookbehind)')[1]);
|
||||
|
||||
this.assertEqual('\\[\\\\w\\]\\+', RegExp.escape('[\\w]+'));
|
||||
this.assertEqual('character class', new RegExp(RegExp.escape('[') + '([^\\]]+)').exec('[character class]')[1]);
|
||||
|
||||
this.assertEqual('<div>', new RegExp(RegExp.escape('<div>')).exec('<td><div></td>')[0]);
|
||||
|
||||
this.assertEqual('false', RegExp.escape(false));
|
||||
this.assertEqual('undefined', RegExp.escape());
|
||||
this.assertEqual('null', RegExp.escape(null));
|
||||
this.assertEqual('42', RegExp.escape(42));
|
||||
|
||||
this.assertEqual('\\\\n\\\\r\\\\t', RegExp.escape('\\n\\r\\t'));
|
||||
this.assertEqual('\n\r\t', RegExp.escape('\n\r\t'));
|
||||
this.assertEqual('\\{5,2\\}', RegExp.escape('{5,2}'));
|
||||
|
||||
this.assertEqual(
|
||||
'\\/\\(\\[\\.\\*\\+\\?\\^\\=\\!\\:\\$\\{\\}\\(\\)\\|\\[\\\\\\]\\\\\\\/\\\\\\\\\\]\\)\\/g',
|
||||
RegExp.escape('/([.*+?^=!:${}()|[\\]\\/\\\\])/g')
|
||||
);
|
||||
},
|
||||
|
||||
testBrowserDetection: function() {
|
||||
var results = $H(Prototype.Browser).map(function(engine){
|
||||
return engine;
|
||||
|
@ -375,7 +25,7 @@ new Test.Unit.Runner({
|
|||
this.assert(Prototype.Browser.WebKit);
|
||||
}
|
||||
|
||||
if(!!window.opera) {
|
||||
if(Object.prototype.toString.call(window.opera) === '[object Opera]') {
|
||||
this.info('Running on Opera');
|
||||
this.assert(Prototype.Browser.Opera);
|
||||
}
|
||||
|
@ -389,134 +39,5 @@ new Test.Unit.Runner({
|
|||
this.info('Running on Gecko');
|
||||
this.assert(Prototype.Browser.Gecko);
|
||||
}
|
||||
},
|
||||
|
||||
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('#<Plant: grass>', 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('#<Ox: cow>', 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());
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
|
@ -112,6 +112,10 @@ new Test.Unit.Runner({
|
|||
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' },
|
||||
|
@ -126,5 +130,7 @@ new Test.Unit.Runner({
|
|||
|
||||
this.assertEqual("toString", new Foo().toString());
|
||||
this.assertEqual("valueOf", new Foo().valueOf());
|
||||
this.assertEqual("toString", new Bar().toString());
|
||||
this.assertEqual("myValueOf", new Bar().valueOf());
|
||||
}
|
||||
});
|
|
@ -73,7 +73,15 @@ new Test.Unit.Runner({
|
|||
|
||||
this.assertElementsMatch(document.getElementsByClassName('A'), 'p.A', 'ul#class_names_ul.A', 'li.A.C');
|
||||
|
||||
if (Prototype.Browser.IE)
|
||||
|
||||
var isElementPrototypeSupported = (function(){
|
||||
var el = document.createElement('div');
|
||||
var result = typeof el.show != 'undefined';
|
||||
el = null;
|
||||
return result;
|
||||
})();
|
||||
|
||||
if (!isElementPrototypeSupported)
|
||||
this.assertUndefined(document.getElementById('unextended').show);
|
||||
|
||||
this.assertElementsMatch(div.getElementsByClassName('B'), 'ul#class_names_ul.A.B', 'div.B.C.D');
|
||||
|
@ -95,7 +103,8 @@ new Test.Unit.Runner({
|
|||
this.assertElementsMatch(list.getElementsByClassName({}));
|
||||
|
||||
// those lookups shouldn't have extended all nodes in document
|
||||
if (Prototype.Browser.IE) this.assertUndefined(document.getElementById('unextended')['show']);
|
||||
if (!isElementPrototypeSupported)
|
||||
this.assertUndefined(document.getElementById('unextended')['show']);
|
||||
},
|
||||
|
||||
testElementInsertWithHTML: function() {
|
||||
|
@ -369,6 +378,15 @@ new Test.Unit.Runner({
|
|||
this.assertEqual('hello world', getInnerHTML('testdiv'));
|
||||
},
|
||||
|
||||
testElementUpdateScriptElement: function() {
|
||||
var el = new Element('script', {
|
||||
type: 'text/javascript'
|
||||
});
|
||||
this.assertNothingRaised(function(){
|
||||
el.update('(function(){})');
|
||||
})
|
||||
},
|
||||
|
||||
testElementReplace: function() {
|
||||
$('testdiv-replace-1').replace('hello from div!');
|
||||
this.assertEqual('hello from div!', $('testdiv-replace-container-1').innerHTML);
|
||||
|
@ -623,6 +641,20 @@ new Test.Unit.Runner({
|
|||
},
|
||||
|
||||
testElementExtend: function() {
|
||||
|
||||
Element.Methods.Simulated.simulatedMethod = function() {
|
||||
return 'simulated';
|
||||
};
|
||||
Element.addMethods();
|
||||
|
||||
function testTag(tagName) {
|
||||
var element = document.createElement(tagName);
|
||||
this.assertEqual(element, Element.extend(element));
|
||||
// test method from Methods
|
||||
this.assertRespondsTo('show', element);
|
||||
// test method from Simulated
|
||||
this.assertRespondsTo('simulatedMethod', element);
|
||||
}
|
||||
var element = $('element_extend_test');
|
||||
this.assertRespondsTo('show', element);
|
||||
|
||||
|
@ -647,6 +679,9 @@ new Test.Unit.Runner({
|
|||
this.assertEqual(textnode, Element.extend(textnode));
|
||||
this.assert(typeof textnode['show'] == 'undefined');
|
||||
}, this);
|
||||
|
||||
// clean up
|
||||
delete Element.Methods.Simulated.simulatedMethod;
|
||||
},
|
||||
|
||||
testElementExtendReextendsDiscardedNodes: function() {
|
||||
|
@ -805,7 +840,29 @@ new Test.Unit.Runner({
|
|||
this.assert(
|
||||
$('style_test_3').setOpacity(0.9999999).getStyle('opacity') > 0.999
|
||||
);
|
||||
if (Prototype.Browser.IE) {
|
||||
|
||||
/*
|
||||
|
||||
IE <= 7 needs a `hasLayout` for opacity ("filter") to function properly
|
||||
`hasLayout` is triggered by setting `zoom` style to `1`,
|
||||
|
||||
In IE8 setting `zoom` does not affect `hasLayout`
|
||||
and IE8 does not even need `hasLayout` for opacity to work
|
||||
|
||||
We do a proper feature test here
|
||||
|
||||
*/
|
||||
|
||||
var ZOOM_AFFECT_HAS_LAYOUT = (function(){
|
||||
// IE7
|
||||
var el = document.createElement('div');
|
||||
el.style.zoom = 1;
|
||||
var result = el.hasLayout;
|
||||
el = null;
|
||||
return result;
|
||||
})();
|
||||
|
||||
if (ZOOM_AFFECT_HAS_LAYOUT) {
|
||||
this.assert($('style_test_4').setOpacity(0.5).currentStyle.hasLayout);
|
||||
this.assert(2, $('style_test_5').setOpacity(0.5).getStyle('zoom'));
|
||||
this.assert(0.5, new Element('div').setOpacity(0.5).getOpacity());
|
||||
|
@ -894,6 +951,9 @@ new Test.Unit.Runner({
|
|||
// height/width could always be calculated if it's set to "auto" (Firefox)
|
||||
this.assertNotNull($('auto_dimensions').getStyle('height'));
|
||||
this.assertNotNull($('auto_dimensions').getStyle('width'));
|
||||
|
||||
// assert that using getStyle on an object without a style property returns null
|
||||
this.assertNull(Element.Methods.getStyle({}, 'width'));
|
||||
},
|
||||
|
||||
testElementGetOpacity: function() {
|
||||
|
@ -907,7 +967,7 @@ new Test.Unit.Runner({
|
|||
this.assertEqual('a link' , $('attributes_with_issues_1').readAttribute('title'));
|
||||
|
||||
$('cloned_element_attributes_issue').readAttribute('foo')
|
||||
var clone = $('cloned_element_attributes_issue').cloneNode(true);
|
||||
var clone = $('cloned_element_attributes_issue').clone(true);
|
||||
clone.writeAttribute('foo', 'cloned');
|
||||
this.assertEqual('cloned', clone.readAttribute('foo'));
|
||||
this.assertEqual('original', $('cloned_element_attributes_issue').readAttribute('foo'));
|
||||
|
@ -1064,13 +1124,20 @@ new Test.Unit.Runner({
|
|||
{id: 'my_input_field_id', name: 'my_input_field'}));
|
||||
this.assertEqual(input, document.body.lastChild);
|
||||
this.assertEqual('my_input_field', $(document.body.lastChild).name);
|
||||
if (Prototype.Browser.IE)
|
||||
this.assertMatch(/name=["']?my_input_field["']?/, $('my_input_field').outerHTML);
|
||||
if ('outerHTML' in document.documentElement) {
|
||||
this.assertMatch(/name=["']?my_input_field["']?/, $('my_input_field_id').outerHTML);
|
||||
}
|
||||
|
||||
if (originalElement && Prototype.BrowserFeatures.ElementExtensions) {
|
||||
Element.prototype.fooBar = Prototype.emptyFunction
|
||||
this.assertRespondsTo('fooBar', new Element('div'));
|
||||
}
|
||||
|
||||
elWithClassName = new Element('div', { 'className': 'firstClassName' });
|
||||
this.assert(elWithClassName.hasClassName('firstClassName'));
|
||||
|
||||
elWithClassName = new Element('div', { 'class': 'firstClassName' });
|
||||
this.assert(elWithClassName.hasClassName('firstClassName'));
|
||||
},
|
||||
|
||||
testElementGetHeight: function() {
|
||||
|
@ -1258,6 +1325,10 @@ new Test.Unit.Runner({
|
|||
this.assertEnumEqual([0,0], offset);
|
||||
this.assertIdentical(0, offset.top);
|
||||
this.assertIdentical(0, offset.left);
|
||||
|
||||
var innerEl = new Element('div'), outerEl = new Element('div');
|
||||
outerEl.appendChild(innerEl);
|
||||
this.assertEnumEqual([0,0], innerEl.cumulativeOffset());
|
||||
},
|
||||
|
||||
testViewportOffset: function() {
|
||||
|
@ -1365,6 +1436,69 @@ new Test.Unit.Runner({
|
|||
constants.each(function(pair) {
|
||||
this.assertEqual(Node[pair.key], pair.value);
|
||||
}, this);
|
||||
},
|
||||
|
||||
testElementStorage: function() {
|
||||
var element = $('test-empty');
|
||||
element.store('foo', 'bar');
|
||||
this.assertEqual("bar", element.retrieve("foo"), "Setting and reading a property");
|
||||
var result = element.store('foo', 'thud');
|
||||
this.assertEqual("thud", element.retrieve("foo"), "Re-setting and reading property");
|
||||
this.assertIdentical(element, result, "Element#store should return element");
|
||||
|
||||
element.store('bar', 'narf');
|
||||
this.assertEnumEqual($w('foo bar'), element.getStorage().keys(), "Getting the storage hash");
|
||||
element.getStorage().unset('bar');
|
||||
this.assertEnumEqual($w('foo'), element.getStorage().keys(), "Getting the storage hash after unsetting a key");
|
||||
|
||||
element.store({ 'narf': 'narf', 'zort': 'zort' });
|
||||
|
||||
this.assertEqual("narf", element.retrieve('narf'), "Storing multiple properties at once");
|
||||
this.assertEqual("zort", element.retrieve('zort'), "Storing multiple properties at once");
|
||||
|
||||
this.assertUndefined(element.retrieve('bar'), "Undefined key should return undefined if default value is not defined");
|
||||
this.assertEqual("default", element.retrieve('bar', 'default'), "Return default value if undefined key");
|
||||
this.assertEqual("default", element.retrieve('bar'), "Makes sure default value has been set properly");
|
||||
|
||||
var clonedElement = $('test-empty').clone(false);
|
||||
this.assert(Object.isUndefined(clonedElement._prototypeUID), "Cloning a node should not confuse the storage engine");
|
||||
},
|
||||
|
||||
testElementClone: function() {
|
||||
var element = new Element('div', {
|
||||
title: 'bar'
|
||||
});
|
||||
element.className = 'foo';
|
||||
|
||||
// add child
|
||||
element.update('<span id="child">child node</span>');
|
||||
|
||||
// add observer
|
||||
element.observe('click', Prototype.emptyFunction);
|
||||
|
||||
// add observer on a child
|
||||
element.down('span').observe('dblclick', Prototype.emptyFunction);
|
||||
|
||||
var shallowClone = element.clone();
|
||||
var deepClone = element.clone(true);
|
||||
|
||||
var assertCloneTraits = (function(clone) {
|
||||
this.assert(clone); // exists
|
||||
this.assert(clone.show); // is extended
|
||||
this.assertEqual('DIV', clone.nodeName.toUpperCase()); // proper nodeName
|
||||
this.assertEqual('foo', clone.className); // proper attributes
|
||||
this.assertEqual('bar', clone.title);
|
||||
this.assert(!clone._prototypeUID); // _prototypeUID does not exist
|
||||
}).bind(this);
|
||||
|
||||
// test generic traits of both deep and shallow clones first
|
||||
assertCloneTraits(shallowClone);
|
||||
assertCloneTraits(deepClone);
|
||||
|
||||
// test deep clone traits
|
||||
this.assert(deepClone.firstChild);
|
||||
this.assertEqual('SPAN', deepClone.firstChild.nodeName.toUpperCase());
|
||||
this.assert(!deepClone.down('span')._prototypeUID);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1380,4 +1514,4 @@ function preservingBrowserDimensions(callback) {
|
|||
} finally {
|
||||
window.resizeTo(original.width, original.height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -136,6 +136,14 @@ new Test.Unit.Runner({
|
|||
|
||||
this.assertEnumEqual($('grepHeader', 'grepCell'),
|
||||
$('grepTable', 'grepTBody', 'grepRow', 'grepHeader', 'grepCell').grep(new Selector('.cell')));
|
||||
|
||||
// troublesome characters
|
||||
this.assertEnumEqual(['?a', 'c?'], ['?a','b','c?'].grep('?'));
|
||||
this.assertEnumEqual(['*a', 'c*'], ['*a','b','c*'].grep('*'));
|
||||
this.assertEnumEqual(['+a', 'c+'], ['+a','b','c+'].grep('+'));
|
||||
this.assertEnumEqual(['{1}a', 'c{1}'], ['{1}a','b','c{1}'].grep('{1}'));
|
||||
this.assertEnumEqual(['(a', 'c('], ['(a','b','c('].grep('('));
|
||||
this.assertEnumEqual(['|a', 'c|'], ['|a','b','c|'].grep('|'));
|
||||
},
|
||||
|
||||
testInclude: function() {
|
||||
|
|
|
@ -109,6 +109,24 @@ new Test.Unit.Runner({
|
|||
span.fire("test:somethingElseHappened");
|
||||
this.assertEqual(2, count);
|
||||
},
|
||||
|
||||
testMultipleEventHandlersCanBeAddedAndRemovedFromAnElement: function() {
|
||||
var span = $("span"), count1 = 0, count2 = 0;
|
||||
var observer1 = function() { count1++ };
|
||||
var observer2 = function() { count2++ };
|
||||
|
||||
span.observe("test:somethingHappened", observer1);
|
||||
span.observe("test:somethingHappened", observer2);
|
||||
span.fire("test:somethingHappened");
|
||||
this.assertEqual(1, count1);
|
||||
this.assertEqual(1, count2);
|
||||
|
||||
span.stopObserving("test:somethingHappened", observer1);
|
||||
span.stopObserving("test:somethingHappened", observer2);
|
||||
span.fire("test:somethingHappened");
|
||||
this.assertEqual(1, count1);
|
||||
this.assertEqual(1, count2);
|
||||
},
|
||||
|
||||
testStopObservingWithoutArguments: function() {
|
||||
var span = $("span"), count = 0, observer = function() { count++ };
|
||||
|
@ -138,23 +156,27 @@ new Test.Unit.Runner({
|
|||
},
|
||||
|
||||
testStopObservingRemovesHandlerFromCache: function() {
|
||||
var span = $("span"), observer = function() { }, eventID;
|
||||
var span = $("span"), observer = Prototype.emptyFunction, eventID;
|
||||
|
||||
span.observe("test:somethingHappened", observer);
|
||||
eventID = span._prototypeEventID;
|
||||
|
||||
this.assert(Event.cache[eventID]);
|
||||
this.assert(Object.isArray(Event.cache[eventID]["test:somethingHappened"]));
|
||||
this.assertEqual(1, Event.cache[eventID]["test:somethingHappened"].length);
|
||||
var registry = span.getStorage().get('prototype_event_registry');
|
||||
|
||||
this.assert(registry);
|
||||
this.assert(Object.isArray(registry.get('test:somethingHappened')));
|
||||
this.assertEqual(1, registry.get('test:somethingHappened').length);
|
||||
|
||||
span.stopObserving("test:somethingHappened", observer);
|
||||
this.assert(Event.cache[eventID]);
|
||||
this.assert(Object.isArray(Event.cache[eventID]["test:somethingHappened"]));
|
||||
this.assertEqual(0, Event.cache[eventID]["test:somethingHappened"].length);
|
||||
|
||||
registry = span.getStorage().get('prototype_event_registry');
|
||||
|
||||
this.assert(registry);
|
||||
this.assert(Object.isArray(registry.get('test:somethingHappened')));
|
||||
this.assertEqual(0, registry.get('test:somethingHappened').length);
|
||||
},
|
||||
|
||||
testObserveAndStopObservingAreChainable: function() {
|
||||
var span = $("span"), observer = function() { };
|
||||
var span = $("span"), observer = Prototype.emptyFunction;
|
||||
|
||||
this.assertEqual(span, span.observe("test:somethingHappened", observer));
|
||||
this.assertEqual(span, span.stopObserving("test:somethingHappened", observer));
|
||||
|
@ -162,6 +184,8 @@ new Test.Unit.Runner({
|
|||
span.observe("test:somethingHappened", observer);
|
||||
this.assertEqual(span, span.stopObserving("test:somethingHappened"));
|
||||
|
||||
this.assertEqual(span, span.stopObserving("test:somethingOtherHappened", observer));
|
||||
|
||||
span.observe("test:somethingHappened", observer);
|
||||
this.assertEqual(span, span.stopObserving());
|
||||
this.assertEqual(span, span.stopObserving()); // assert it again, after there are no observers
|
||||
|
@ -188,7 +212,7 @@ new Test.Unit.Runner({
|
|||
testEventStopped: function() {
|
||||
var span = $("span"), event;
|
||||
|
||||
span.observe("test:somethingHappened", function() { });
|
||||
span.observe("test:somethingHappened", Prototype.emptyFunction);
|
||||
event = span.fire("test:somethingHappened");
|
||||
this.assert(!event.stopped, "event.stopped should be false with an empty observer");
|
||||
span.stopObserving("test:somethingHappened");
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
<div id="test"></div>
|
||||
<ul id="list">
|
||||
<li></li>
|
||||
<li></li>
|
||||
<li></li>
|
||||
</ul>
|
|
@ -1,106 +0,0 @@
|
|||
var Person = function(name){
|
||||
this.name = name;
|
||||
};
|
||||
|
||||
Person.prototype.toJSON = function() {
|
||||
return '-' + this.name;
|
||||
};
|
||||
|
||||
var arg1 = 1;
|
||||
var arg2 = 2;
|
||||
var arg3 = 3;
|
||||
function TestObj() { };
|
||||
TestObj.prototype.assertingEventHandler =
|
||||
function(event, assertEvent, assert1, assert2, assert3, a1, a2, a3) {
|
||||
assertEvent(event);
|
||||
assert1(a1);
|
||||
assert2(a2);
|
||||
assert3(a3);
|
||||
};
|
||||
|
||||
var globalBindTest = null;
|
||||
|
||||
|
||||
// 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 '#<Sellable: #{weight}kg>'.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 '#<Plant: #{name}>'.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 '#<Ox: #{name}>'.interpolate(this);
|
||||
}
|
||||
});
|
|
@ -53,6 +53,17 @@
|
|||
</span>
|
||||
</span>
|
||||
</div> <!-- #dupContainer -->
|
||||
<div id="dupContainer.withdot:active">
|
||||
<span id="dupL1_dotcolon" class="span_foo span_bar">
|
||||
<span id="dupL2_dotcolon">
|
||||
<span id="dupL3_dotcolon">
|
||||
<span id="dupL4_dotcolon">
|
||||
<span id="dupL5_dotcolon"></span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div> <!-- #dupContainer.withdot:active -->
|
||||
|
||||
<div id="grandfather"> grandfather
|
||||
<div id="father" class="brothers men"> father
|
||||
|
@ -66,4 +77,12 @@
|
|||
<input type="hidden" id="commaTwo" name="foo2" value="oops" />
|
||||
</form>
|
||||
<div id="counted_container"><div class="is_counted"></div></div>
|
||||
|
||||
<div foo-bar="baz" id="attr_with_dash">blah</div>
|
||||
|
||||
<div id="container_1" class="container">
|
||||
<div id="container_2" class="container">
|
||||
<span id="target_1"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
<div id="test_div_parent" class="test_class">
|
||||
<div id="test_div_child" class="test_class">
|
||||
</div>
|
||||
</div>
|
|
@ -26,6 +26,10 @@ new Test.Unit.Runner({
|
|||
this.assertEqual("4", $F("input_enabled"));
|
||||
},
|
||||
|
||||
testFormReset: function() {
|
||||
this.assert(!Object.isUndefined(Form.reset('form').reset));
|
||||
},
|
||||
|
||||
testFormElementEventObserver: function(){
|
||||
var callbackCounter = 0;
|
||||
var observer = new Form.Element.EventObserver('input_enabled', function(){
|
||||
|
@ -287,8 +291,8 @@ new Test.Unit.Runner({
|
|||
|
||||
form = $('bigform');
|
||||
var input = form['tf_text'], select = form['tf_selectOne'];
|
||||
input._extendedByPrototype = select._extendedByPrototype = false;
|
||||
|
||||
input._extendedByPrototype = select._extendedByPrototype = void 0;
|
||||
|
||||
this.assert($(input).anInputMethod);
|
||||
this.assert(!input.aSelectMethod);
|
||||
this.assertEqual('input', input.anInputMethod());
|
||||
|
@ -313,7 +317,7 @@ new Test.Unit.Runner({
|
|||
|
||||
// with empty action attribute
|
||||
request = $("ffe").request({ method: 'post' });
|
||||
this.assert(request.url.include("unit/tmp/form_test.html"),
|
||||
this.assert(request.url.include("/tmp/form_test.html"),
|
||||
'wrong default action for form element with empty action attribute');
|
||||
},
|
||||
|
||||
|
@ -355,5 +359,24 @@ new Test.Unit.Runner({
|
|||
'multiple select options improperly set');
|
||||
input.setValue(['1', '3']);
|
||||
this.assertEnumEqual(['1', '3'], input.getValue());
|
||||
},
|
||||
|
||||
testSerializeFormTroublesomeNames: function() {
|
||||
var el = new Element('form', {
|
||||
action: '/'
|
||||
});
|
||||
var input = new Element('input', {
|
||||
type: 'text',
|
||||
name: 'length',
|
||||
value: 'foo'
|
||||
});
|
||||
var input2 = new Element('input', {
|
||||
type: 'text',
|
||||
name: 'bar',
|
||||
value: 'baz'
|
||||
});
|
||||
el.appendChild(input);
|
||||
el.appendChild(input2);
|
||||
this.assertHashEqual({ length: 'foo', bar: 'baz' }, el.serialize(true));
|
||||
}
|
||||
});
|
|
@ -129,22 +129,28 @@ new Test.Unit.Runner({
|
|||
testObjectIsString: function() {
|
||||
this.assert(!Object.isString(function() { }));
|
||||
this.assert(Object.isString("a string"));
|
||||
this.assert(Object.isString(new String("a string")));
|
||||
this.assert(!Object.isString(0));
|
||||
this.assert(!Object.isString([]));
|
||||
this.assert(!Object.isString({}));
|
||||
this.assert(!Object.isString(false));
|
||||
this.assert(!Object.isString(undefined));
|
||||
this.assert(!Object.isString(document), 'host objects should return false rather than throw exceptions');
|
||||
},
|
||||
|
||||
testObjectIsNumber: function() {
|
||||
this.assert(Object.isNumber(0));
|
||||
this.assert(Object.isNumber(1.0));
|
||||
this.assert(Object.isNumber(new Number(0)));
|
||||
this.assert(Object.isNumber(new Number(1.0)));
|
||||
this.assert(!Object.isNumber(function() { }));
|
||||
this.assert(!Object.isNumber({ test: function() { return 3 } }));
|
||||
this.assert(!Object.isNumber("a string"));
|
||||
this.assert(!Object.isNumber([]));
|
||||
this.assert(!Object.isNumber({}));
|
||||
this.assert(!Object.isNumber(false));
|
||||
this.assert(!Object.isNumber(undefined));
|
||||
this.assert(!Object.isNumber(document), 'host objects should return false rather than throw exceptions');
|
||||
},
|
||||
|
||||
testObjectIsUndefined: function() {
|
||||
|
|
|
@ -11,5 +11,25 @@ new Test.Unit.Runner({
|
|||
this.wait(600, function() {
|
||||
this.assertEqual(3, peEventCount);
|
||||
});
|
||||
},
|
||||
|
||||
testOnTimerEventMethod: function() {
|
||||
var testcase = this,
|
||||
pe = {
|
||||
onTimerEvent: PeriodicalExecuter.prototype.onTimerEvent,
|
||||
execute: function() {
|
||||
testcase.assert(pe.currentlyExecuting);
|
||||
}
|
||||
};
|
||||
|
||||
pe.onTimerEvent();
|
||||
this.assert(!pe.currentlyExecuting);
|
||||
|
||||
pe.execute = function() {
|
||||
testcase.assert(pe.currentlyExecuting);
|
||||
throw new Error()
|
||||
}
|
||||
this.assertRaise('Error', pe.onTimerEvent.bind(pe));
|
||||
this.assert(!pe.currentlyExecuting);
|
||||
}
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
<div id="test_div_parent" class="test_class">
|
||||
<div id="test_div_child" class="test_class">
|
||||
</div>
|
||||
</div>
|
||||
*/
|
||||
|
||||
new Test.Unit.Runner({
|
||||
testEngine: function() {
|
||||
this.assert(Prototype.Selector.engine);
|
||||
},
|
||||
|
||||
testSelect: function() {
|
||||
var elements = Prototype.Selector.select('.test_class');
|
||||
|
||||
this.assert(Object.isArray(elements));
|
||||
this.assertEqual(2, elements.length);
|
||||
this.assertEqual('test_div_parent', elements[0].id);
|
||||
this.assertEqual('test_div_child', elements[1].id);
|
||||
},
|
||||
|
||||
testSelectWithContext: function() {
|
||||
var elements = Prototype.Selector.select('.test_class', $('test_div_parent'));
|
||||
|
||||
this.assert(Object.isArray(elements));
|
||||
this.assertEqual(1, elements.length);
|
||||
this.assertEqual('test_div_child', elements[0].id);
|
||||
},
|
||||
|
||||
testSelectWithEmptyResult: function() {
|
||||
var elements = Prototype.Selector.select('.non_existent');
|
||||
|
||||
this.assert(Object.isArray(elements));
|
||||
this.assertEqual(0, elements.length);
|
||||
},
|
||||
|
||||
testMatch: function() {
|
||||
var element = $('test_div_parent');
|
||||
|
||||
this.assertEqual(true, Prototype.Selector.match(element, '.test_class'));
|
||||
this.assertEqual(false, Prototype.Selector.match(element, '.non_existent'));
|
||||
},
|
||||
|
||||
testFind: function() {
|
||||
var elements = document.getElementsByTagName('*'),
|
||||
expression = '.test_class';
|
||||
this.assertEqual('test_div_parent', Prototype.Selector.find(elements, expression).id);
|
||||
this.assertEqual('test_div_child', Prototype.Selector.find(elements, expression, 1).id);
|
||||
}
|
||||
});
|
|
@ -1,5 +1,9 @@
|
|||
var $RunBenchmarks = false;
|
||||
|
||||
function reduce(arr) {
|
||||
return arr.length > 1 ? arr : arr[0];
|
||||
}
|
||||
|
||||
new Test.Unit.Runner({
|
||||
|
||||
testSelectorWithTagName: function() {
|
||||
|
@ -87,6 +91,10 @@ new Test.Unit.Runner({
|
|||
this.assertEnumEqual($$('#troubleForm *[type]'), $$('#troubleForm [type]'));
|
||||
},
|
||||
|
||||
testSelectorWithAttributeContainingDash: function() {
|
||||
this.assertEnumEqual([$('attr_with_dash')], $$('[foo-bar]'), "attribute with hyphen");
|
||||
},
|
||||
|
||||
testSelectorWithUniversalAndHyphenTokenizedAttributeValue: function() {
|
||||
this.assertEnumEqual([$('item_3')], $$('*[xml:lang|="es"]'));
|
||||
this.assertEnumEqual([$('item_3')], $$('*[xml:lang|="ES"]'));
|
||||
|
@ -189,12 +197,12 @@ new Test.Unit.Runner({
|
|||
testSelectorWithAdjacence: function() {
|
||||
this.assertEnumEqual([$('uncle')], $$('div.brothers + div.brothers'));
|
||||
this.assertEnumEqual([$('uncle')], $$('div.brothers + div'));
|
||||
this.assertEqual($('level2_2'), $$('#level2_1+span').reduce());
|
||||
this.assertEqual($('level2_2'), $$('#level2_1 + span').reduce());
|
||||
this.assertEqual($('level2_2'), $$('#level2_1 + *').reduce());
|
||||
this.assertEqual($('level2_2'), reduce($$('#level2_1+span')));
|
||||
this.assertEqual($('level2_2'), reduce($$('#level2_1 + span')));
|
||||
this.assertEqual($('level2_2'), reduce($$('#level2_1 + *')));
|
||||
this.assertEnumEqual([], $$('#level2_2 + span'));
|
||||
this.assertEqual($('level3_2'), $$('#level3_1 + span').reduce());
|
||||
this.assertEqual($('level3_2'), $$('#level3_1 + *').reduce());
|
||||
this.assertEqual($('level3_2'), reduce($$('#level3_1 + span')));
|
||||
this.assertEqual($('level3_2'), reduce($$('#level3_1 + *')));
|
||||
this.assertEnumEqual([], $$('#level3_2 + *'));
|
||||
this.assertEnumEqual([], $$('#level3_1 + em'));
|
||||
$RunBenchmarks && this.wait(500, function() {
|
||||
|
@ -204,8 +212,8 @@ new Test.Unit.Runner({
|
|||
|
||||
testSelectorWithLaterSibling: function() {
|
||||
this.assertEnumEqual([$('list')], $$('h1 ~ ul'));
|
||||
this.assertEqual($('level2_2'), $$('#level2_1 ~ span').reduce());
|
||||
this.assertEnumEqual($('level2_2', 'level2_3'), $$('#level2_1 ~ *').reduce());
|
||||
this.assertEqual($('level2_2'), reduce($$('#level2_1 ~ span')));
|
||||
this.assertEnumEqual($('level2_2', 'level2_3'), reduce($$('#level2_1 ~ *')));
|
||||
this.assertEnumEqual([], $$('#level2_2 ~ span'));
|
||||
this.assertEnumEqual([], $$('#level3_2 ~ *'));
|
||||
this.assertEnumEqual([], $$('#level3_1 ~ em'));
|
||||
|
@ -348,14 +356,6 @@ new Test.Unit.Runner({
|
|||
this.assert(typeof results[2].show == 'function');
|
||||
},
|
||||
|
||||
testCountedIsNotAnAttribute: function() {
|
||||
var el = $('list');
|
||||
Selector.handlers.mark([el]);
|
||||
this.assert(!el.innerHTML.include("_counted"));
|
||||
Selector.handlers.unmark([el]);
|
||||
this.assert(!el.innerHTML.include("_counted"));
|
||||
},
|
||||
|
||||
testCopiedNodesGetIncluded: function() {
|
||||
this.assertElementsMatch(
|
||||
Selector.matchElements($('counted_container').descendants(), 'div'),
|
||||
|
@ -367,11 +367,48 @@ new Test.Unit.Runner({
|
|||
'div.is_counted'
|
||||
);
|
||||
},
|
||||
|
||||
testSelectorNotInsertedNodes: function() {
|
||||
window.debug = true;
|
||||
var wrapper = new Element("div");
|
||||
wrapper.update("<table><tr><td id='myTD'></td></tr></table>");
|
||||
this.assertNotNullOrUndefined(wrapper.select('[id=myTD]')[0],
|
||||
'selecting: [id=myTD]');
|
||||
this.assertNotNullOrUndefined(wrapper.select('#myTD')[0],
|
||||
'selecting: #myTD');
|
||||
this.assertNotNullOrUndefined(wrapper.select('td')[0],
|
||||
'selecting: td');
|
||||
this.assert($$('#myTD').length == 0,
|
||||
'should not turn up in document-rooted search');
|
||||
window.debug = false;
|
||||
},
|
||||
|
||||
testElementDown: function() {
|
||||
var a = $('dupL4');
|
||||
var b = $('dupContainer').down('#dupL4');
|
||||
|
||||
this.assertEqual(a, b);
|
||||
},
|
||||
testElementDownWithDotAndColon: function() {
|
||||
var a = $('dupL4_dotcolon');
|
||||
var b = $('dupContainer.withdot:active').down('#dupL4_dotcolon');
|
||||
var c = $('dupContainer.withdot:active').select('#dupL4_dotcolon');
|
||||
|
||||
this.assertEqual(a, b);
|
||||
this.assertEnumEqual([a], c);
|
||||
},
|
||||
|
||||
testDescendantSelectorBuggy: function() {
|
||||
var el = document.createElement('div');
|
||||
el.innerHTML = '<ul><li></li></ul><div><ul><li></li></ul></div>';
|
||||
document.body.appendChild(el);
|
||||
this.assertEqual(2, $(el).select('ul li').length);
|
||||
document.body.removeChild(el);
|
||||
},
|
||||
|
||||
testFindElementWithIndexWhenElementsAreNotInDocumentOrder: function() {
|
||||
var ancestors = $("target_1").ancestors();
|
||||
this.assertEqual($("container_2"), Selector.findElement(ancestors, "[container], .container", 0));
|
||||
this.assertEqual($("container_1"), Selector.findElement(ancestors, "[container], .container", 1));
|
||||
}
|
||||
});
|
|
@ -44,6 +44,11 @@ new Test.Unit.Runner({
|
|||
'ウィメンズ2007\nクルーズコレクション'.gsub(/\n/,'<br/>'));
|
||||
this.assertEqual('ウィメンズ2007<br/>クルーズコレクション',
|
||||
'ウィメンズ2007\nクルーズコレクション'.gsub('\n','<br/>'));
|
||||
|
||||
this.assertEqual('barfbarobarobar barbbarobarobar barbbarobarzbar',
|
||||
source.gsub('', 'bar'));
|
||||
this.assertEqual('barfbarobarobar barbbarobarobar barbbarobarzbar',
|
||||
source.gsub(new RegExp(''), 'bar'));
|
||||
},
|
||||
|
||||
testGsubWithReplacementTemplateString: function() {
|
||||
|
@ -59,6 +64,19 @@ new Test.Unit.Runner({
|
|||
source.gsub(/(.)(o+)/, '#{3}'));
|
||||
},
|
||||
|
||||
testGsubWithTroublesomeCharacters: function() {
|
||||
this.assertEqual('ab', 'a|b'.gsub('|', ''));
|
||||
//'ab'.gsub('', ''); // freeze
|
||||
this.assertEqual('ab', 'ab(?:)'.gsub('(?:)', ''));
|
||||
this.assertEqual('ab', 'ab()'.gsub('()', ''));
|
||||
this.assertEqual('ab', 'ab'.gsub('^', ''));
|
||||
this.assertEqual('ab', 'a?b'.gsub('?', ''))
|
||||
this.assertEqual('ab', 'a+b'.gsub('+', ''));
|
||||
this.assertEqual('ab', 'a*b'.gsub('*', ''));
|
||||
this.assertEqual('ab', 'a{1}b'.gsub('{1}', ''));
|
||||
this.assertEqual('ab', 'a.b'.gsub('.', ''));
|
||||
},
|
||||
|
||||
testSubWithReplacementFunction: function() {
|
||||
var source = 'foo boo boz';
|
||||
|
||||
|
@ -190,6 +208,7 @@ new Test.Unit.Runner({
|
|||
this.assertEqual('hello world', '<a href="#" onclick="moo!">hello</a> world'.stripTags());
|
||||
this.assertEqual('hello world', 'h<b><em>e</em></b>l<i>l</i>o w<span class="moo" id="x"><b>o</b></span>rld'.stripTags());
|
||||
this.assertEqual('1\n2', '1\n2'.stripTags());
|
||||
this.assertEqual('one < two blah baz', 'one < two <a href="#" title="foo > bar">blah</a> <input disabled>baz'.stripTags());
|
||||
},
|
||||
|
||||
testStripScripts: function() {
|
||||
|
@ -250,6 +269,8 @@ new Test.Unit.Runner({
|
|||
this.assertEqual('1\n2', '1\n2'.unescapeHTML());
|
||||
this.assertEqual('Pride & Prejudice', '<h1>Pride & Prejudice</h1>'.unescapeHTML());
|
||||
|
||||
this.assertIdentical('<', '&lt;'.unescapeHTML());
|
||||
|
||||
this.benchmark(function() { largeTextEscaped.unescapeHTML() }, 1000);
|
||||
|
||||
},
|
||||
|
@ -296,20 +317,20 @@ new Test.Unit.Runner({
|
|||
},
|
||||
|
||||
testTemplateEvaluationWithIndexing: function() {
|
||||
var source = '#{0} = #{[0]} - #{1} = #{[1]} - #{[2][0]} - #{[2].name} - #{first[0]} - #{[first][0]} - #{[\\]]} - #{first[\\]]}';
|
||||
var source = '#{0} = #{[0]} - #{1} = #{[1]} - #{[2][0]} - #{[2].name} - #{first[0]} - #{[first][0]} - #{[\]]} - #{first[\]]}';
|
||||
var subject = [ 'zero', 'one', [ 'two-zero' ] ];
|
||||
subject[2].name = 'two-zero-name';
|
||||
subject.first = subject[2];
|
||||
subject[']'] = '\\';
|
||||
subject.first[']'] = 'first\\';
|
||||
this.assertEqual('zero', new Template('#{[0]}').evaluate(subject));
|
||||
this.assertEqual('one', new Template('#{[1]}').evaluate(subject));
|
||||
this.assertEqual('two-zero', new Template('#{[2][0]}').evaluate(subject));
|
||||
this.assertEqual('two-zero-name', new Template('#{[2].name}').evaluate(subject));
|
||||
this.assertEqual('two-zero', new Template('#{first[0]}').evaluate(subject));
|
||||
this.assertEqual('\\', new Template('#{[\\]]}').evaluate(subject));
|
||||
this.assertEqual('first\\', new Template('#{first[\\]]}').evaluate(subject));
|
||||
this.assertEqual('empty - empty2', new Template('#{[]} - #{m[]}').evaluate({ '': 'empty', m: {'': 'empty2'}}));
|
||||
this.assertEqual('zero', new Template('#{[0]}').evaluate(subject), "#{[0]}");
|
||||
this.assertEqual('one', new Template('#{[1]}').evaluate(subject), "#{[1]}");
|
||||
this.assertEqual('two-zero', new Template('#{[2][0]}').evaluate(subject), '#{[2][0]}');
|
||||
this.assertEqual('two-zero-name', new Template('#{[2].name}').evaluate(subject), '#{[2].name}');
|
||||
this.assertEqual('two-zero', new Template('#{first[0]}').evaluate(subject), '#{first[0]}');
|
||||
this.assertEqual('\\', new Template('#{[\]]}').evaluate(subject), '#{[\]]}');
|
||||
this.assertEqual('first\\', new Template('#{first[\]]}').evaluate(subject), '#{first[\]]}');
|
||||
this.assertEqual('empty - empty2', new Template('#{[]} - #{m[]}').evaluate({ '': 'empty', m: {'': 'empty2'}}), '#{[]} - #{m[]}');
|
||||
this.assertEqual('zero = zero - one = one - two-zero - two-zero-name - two-zero - two-zero - \\ - first\\', new Template(source).evaluate(subject));
|
||||
},
|
||||
|
||||
|
@ -321,6 +342,12 @@ new Test.Unit.Runner({
|
|||
toTemplateReplacements: function() { return { name: this.name, job: this.getJob() } }
|
||||
};
|
||||
this.assertEqual('My name is Stephan, my job is Web developer', new Template(source).evaluate(subject));
|
||||
|
||||
var strActual = new Template('foo #{bar} baz').evaluate({
|
||||
toTemplateReplacements: function(){ return null; }
|
||||
});
|
||||
this.assertIdentical('foo baz', strActual);
|
||||
this.assertIdentical('foo', new Template('foo#{bar}').evaluate(null));
|
||||
},
|
||||
|
||||
testTemplateEvaluationCombined: function() {
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<title>Unit test file | <%= title %> | <%= template_name %> template | <%= timestamp %></title>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
var eventResults = {};
|
||||
var originalElement = window.Element;
|
||||
</script>
|
||||
<%= script_tag('assets/prototype.js') %>
|
||||
<%= script_tag('lib_assets/unittest.js') %>
|
||||
<%= link_tag('lib_assets/unittest.css') %>
|
||||
<%= css_fixtures %>
|
||||
<%= js_fixtures %>
|
||||
<%= test_file %>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="testlog"></div>
|
||||
|
||||
<%= html_fixtures %>
|
||||
</body>
|
||||
</html>
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
eventResults.endOfDocument = true;
|
||||
</script>
|
|
@ -0,0 +1 @@
|
|||
Subproject commit aeda517c2e82db92bc88d56ed68fa4ce05f487a9
|
|
@ -0,0 +1,803 @@
|
|||
/* Portions of the Prototype.LegacySelector class are derived from Jack Slocum's DomQuery,
|
||||
* part of YUI-Ext version 0.40, distributed under the terms of an MIT-style
|
||||
* license. Please see http://www.yui-ext.com/ for more information. */
|
||||
|
||||
Prototype.LegacySelector = Class.create({
|
||||
initialize: function(expression) {
|
||||
this.expression = expression.strip();
|
||||
|
||||
if (this.shouldUseSelectorsAPI()) {
|
||||
this.mode = 'selectorsAPI';
|
||||
} else if (this.shouldUseXPath()) {
|
||||
this.mode = 'xpath';
|
||||
this.compileXPathMatcher();
|
||||
} else {
|
||||
this.mode = "normal";
|
||||
this.compileMatcher();
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
shouldUseXPath: (function() {
|
||||
|
||||
// Some versions of Opera 9.x produce incorrect results when using XPath
|
||||
// with descendant combinators.
|
||||
// see: http://opera.remcol.ath.cx/bugs/index.php?action=bug&id=652
|
||||
var IS_DESCENDANT_SELECTOR_BUGGY = (function(){
|
||||
var isBuggy = false;
|
||||
if (document.evaluate && window.XPathResult) {
|
||||
var el = document.createElement('div');
|
||||
el.innerHTML = '<ul><li></li></ul><div><ul><li></li></ul></div>';
|
||||
|
||||
var xpath = ".//*[local-name()='ul' or local-name()='UL']" +
|
||||
"//*[local-name()='li' or local-name()='LI']";
|
||||
|
||||
var result = document.evaluate(xpath, el, null,
|
||||
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
|
||||
|
||||
isBuggy = (result.snapshotLength !== 2);
|
||||
el = null;
|
||||
}
|
||||
return isBuggy;
|
||||
})();
|
||||
|
||||
return 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;
|
||||
|
||||
if (IS_DESCENDANT_SELECTOR_BUGGY) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
})(),
|
||||
|
||||
shouldUseSelectorsAPI: function() {
|
||||
if (!Prototype.BrowserFeatures.SelectorsAPI) return false;
|
||||
|
||||
if (Prototype.LegacySelector.CASE_INSENSITIVE_CLASS_NAMES) return false;
|
||||
|
||||
if (!Prototype.LegacySelector._div) Prototype.LegacySelector._div = new Element('div');
|
||||
|
||||
// Make sure the browser treats the selector as valid. Test on an
|
||||
// isolated element to minimize cost of this check.
|
||||
try {
|
||||
Prototype.LegacySelector._div.querySelector(this.expression);
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
compileMatcher: function() {
|
||||
var e = this.expression, ps = Prototype.LegacySelector.patterns, h = Prototype.LegacySelector.handlers,
|
||||
c = Prototype.LegacySelector.criteria, le, p, m, len = ps.length, name;
|
||||
|
||||
if (Prototype.LegacySelector._cache[e]) {
|
||||
this.matcher = Prototype.LegacySelector._cache[e];
|
||||
return;
|
||||
}
|
||||
|
||||
this.matcher = ["this.matcher = function(root) {",
|
||||
"var r = root, h = Prototype.LegacySelector.handlers, c = false, n;"];
|
||||
|
||||
while (e && le != e && (/\S/).test(e)) {
|
||||
le = e;
|
||||
for (var i = 0; i<len; i++) {
|
||||
p = ps[i].re;
|
||||
name = ps[i].name;
|
||||
if (m = e.match(p)) {
|
||||
this.matcher.push(Object.isFunction(c[name]) ? c[name](m) :
|
||||
new Template(c[name]).evaluate(m));
|
||||
e = e.replace(m[0], '');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.matcher.push("return h.unique(n);\n}");
|
||||
eval(this.matcher.join('\n'));
|
||||
Prototype.LegacySelector._cache[this.expression] = this.matcher;
|
||||
},
|
||||
|
||||
compileXPathMatcher: function() {
|
||||
var e = this.expression, ps = Prototype.LegacySelector.patterns,
|
||||
x = Prototype.LegacySelector.xpath, le, m, len = ps.length, name;
|
||||
|
||||
if (Prototype.LegacySelector._cache[e]) {
|
||||
this.xpath = Prototype.LegacySelector._cache[e]; return;
|
||||
}
|
||||
|
||||
this.matcher = ['.//*'];
|
||||
while (e && le != e && (/\S/).test(e)) {
|
||||
le = e;
|
||||
for (var i = 0; i<len; i++) {
|
||||
name = ps[i].name;
|
||||
if (m = e.match(ps[i].re)) {
|
||||
this.matcher.push(Object.isFunction(x[name]) ? x[name](m) :
|
||||
new Template(x[name]).evaluate(m));
|
||||
e = e.replace(m[0], '');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.xpath = this.matcher.join('');
|
||||
Prototype.LegacySelector._cache[this.expression] = this.xpath;
|
||||
},
|
||||
|
||||
findElements: function(root) {
|
||||
root = root || document;
|
||||
var e = this.expression, results;
|
||||
|
||||
switch (this.mode) {
|
||||
case 'selectorsAPI':
|
||||
// querySelectorAll queries document-wide, then filters to descendants
|
||||
// of the context element. That's not what we want.
|
||||
// Add an explicit context to the selector if necessary.
|
||||
if (root !== document) {
|
||||
var oldId = root.id, id = $(root).identify();
|
||||
// Escape special characters in the ID.
|
||||
id = id.replace(/([\.:])/g, "\\$1");
|
||||
e = "#" + id + " " + e;
|
||||
}
|
||||
|
||||
results = $A(root.querySelectorAll(e)).map(Element.extend);
|
||||
root.id = oldId;
|
||||
|
||||
return results;
|
||||
case 'xpath':
|
||||
return document._getElementsByXPath(this.xpath, root);
|
||||
default:
|
||||
return this.matcher(root);
|
||||
}
|
||||
},
|
||||
|
||||
match: function(element) {
|
||||
this.tokens = [];
|
||||
|
||||
var e = this.expression, ps = Prototype.LegacySelector.patterns, as = Prototype.LegacySelector.assertions;
|
||||
var le, p, m, len = ps.length, name;
|
||||
|
||||
while (e && le !== e && (/\S/).test(e)) {
|
||||
le = e;
|
||||
for (var i = 0; i<len; i++) {
|
||||
p = ps[i].re;
|
||||
name = ps[i].name;
|
||||
if (m = e.match(p)) {
|
||||
// use the Prototype.LegacySelector.assertions methods unless the selector
|
||||
// is too complex.
|
||||
if (as[name]) {
|
||||
this.tokens.push([name, Object.clone(m)]);
|
||||
e = e.replace(m[0], '');
|
||||
} else {
|
||||
// reluctantly do a document-wide search
|
||||
// and look for a match in the array
|
||||
return this.findElements(document).include(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var match = true, name, matches;
|
||||
for (var i = 0, token; token = this.tokens[i]; i++) {
|
||||
name = token[0], matches = token[1];
|
||||
if (!Prototype.LegacySelector.assertions[name](element, matches)) {
|
||||
match = false; break;
|
||||
}
|
||||
}
|
||||
|
||||
return match;
|
||||
},
|
||||
|
||||
toString: function() {
|
||||
return this.expression;
|
||||
},
|
||||
|
||||
inspect: function() {
|
||||
return "#<Prototype.LegacySelector:" + this.expression.inspect() + ">";
|
||||
}
|
||||
});
|
||||
|
||||
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.
|
||||
Prototype.LegacySelector.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(Prototype.LegacySelector, {
|
||||
_cache: { },
|
||||
|
||||
xpath: {
|
||||
descendant: "//*",
|
||||
child: "/*",
|
||||
adjacent: "/following-sibling::*[1]",
|
||||
laterSibling: '/following-sibling::*',
|
||||
tagName: function(m) {
|
||||
if (m[1] == '*') return '';
|
||||
return "[local-name()='" + m[1].toLowerCase() +
|
||||
"' or local-name()='" + m[1].toUpperCase() + "']";
|
||||
},
|
||||
className: "[contains(concat(' ', @class, ' '), ' #{1} ')]",
|
||||
id: "[@id='#{1}']",
|
||||
attrPresence: function(m) {
|
||||
m[1] = m[1].toLowerCase();
|
||||
return new Template("[@#{1}]").evaluate(m);
|
||||
},
|
||||
attr: function(m) {
|
||||
m[1] = m[1].toLowerCase();
|
||||
m[3] = m[5] || m[6];
|
||||
return new Template(Prototype.LegacySelector.xpath.operators[m[2]]).evaluate(m);
|
||||
},
|
||||
pseudo: function(m) {
|
||||
var h = Prototype.LegacySelector.xpath.pseudos[m[1]];
|
||||
if (!h) return '';
|
||||
if (Object.isFunction(h)) return h(m);
|
||||
return new Template(Prototype.LegacySelector.xpath.pseudos[m[1]]).evaluate(m);
|
||||
},
|
||||
operators: {
|
||||
'=': "[@#{1}='#{3}']",
|
||||
'!=': "[@#{1}!='#{3}']",
|
||||
'^=': "[starts-with(@#{1}, '#{3}')]",
|
||||
'$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']",
|
||||
'*=': "[contains(@#{1}, '#{3}')]",
|
||||
'~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]",
|
||||
'|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]"
|
||||
},
|
||||
pseudos: {
|
||||
'first-child': '[not(preceding-sibling::*)]',
|
||||
'last-child': '[not(following-sibling::*)]',
|
||||
'only-child': '[not(preceding-sibling::* or following-sibling::*)]',
|
||||
'empty': "[count(*) = 0 and (count(text()) = 0)]",
|
||||
'checked': "[@checked]",
|
||||
'disabled': "[(@disabled) and (@type!='hidden')]",
|
||||
'enabled': "[not(@disabled) and (@type!='hidden')]",
|
||||
'not': function(m) {
|
||||
var e = m[6], p = Prototype.LegacySelector.patterns,
|
||||
x = Prototype.LegacySelector.xpath, le, v, len = p.length, name;
|
||||
|
||||
var exclusion = [];
|
||||
while (e && le != e && (/\S/).test(e)) {
|
||||
le = e;
|
||||
for (var i = 0; i<len; i++) {
|
||||
name = p[i].name;
|
||||
if (m = e.match(p[i].re)) {
|
||||
v = Object.isFunction(x[name]) ? x[name](m) : new Template(x[name]).evaluate(m);
|
||||
exclusion.push("(" + v.substring(1, v.length - 1) + ")");
|
||||
e = e.replace(m[0], '');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "[not(" + exclusion.join(" and ") + ")]";
|
||||
},
|
||||
'nth-child': function(m) {
|
||||
return Prototype.LegacySelector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m);
|
||||
},
|
||||
'nth-last-child': function(m) {
|
||||
return Prototype.LegacySelector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m);
|
||||
},
|
||||
'nth-of-type': function(m) {
|
||||
return Prototype.LegacySelector.xpath.pseudos.nth("position() ", m);
|
||||
},
|
||||
'nth-last-of-type': function(m) {
|
||||
return Prototype.LegacySelector.xpath.pseudos.nth("(last() + 1 - position()) ", m);
|
||||
},
|
||||
'first-of-type': function(m) {
|
||||
m[6] = "1"; return Prototype.LegacySelector.xpath.pseudos['nth-of-type'](m);
|
||||
},
|
||||
'last-of-type': function(m) {
|
||||
m[6] = "1"; return Prototype.LegacySelector.xpath.pseudos['nth-last-of-type'](m);
|
||||
},
|
||||
'only-of-type': function(m) {
|
||||
var p = Prototype.LegacySelector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m);
|
||||
},
|
||||
nth: function(fragment, m) {
|
||||
var mm, formula = m[6], predicate;
|
||||
if (formula == 'even') formula = '2n+0';
|
||||
if (formula == 'odd') formula = '2n+1';
|
||||
if (mm = formula.match(/^(\d+)$/)) // digit only
|
||||
return '[' + fragment + "= " + mm[1] + ']';
|
||||
if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
|
||||
if (mm[1] == "-") mm[1] = -1;
|
||||
var a = mm[1] ? Number(mm[1]) : 1;
|
||||
var b = mm[2] ? Number(mm[2]) : 0;
|
||||
predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " +
|
||||
"((#{fragment} - #{b}) div #{a} >= 0)]";
|
||||
return new Template(predicate).evaluate({
|
||||
fragment: fragment, a: a, b: b });
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
criteria: {
|
||||
tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;',
|
||||
className: 'n = h.className(n, r, "#{1}", c); c = false;',
|
||||
id: 'n = h.id(n, r, "#{1}", c); c = false;',
|
||||
attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;',
|
||||
attr: function(m) {
|
||||
m[3] = (m[5] || m[6]);
|
||||
return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m);
|
||||
},
|
||||
pseudo: function(m) {
|
||||
if (m[6]) m[6] = m[6].replace(/"/g, '\\"');
|
||||
return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m);
|
||||
},
|
||||
descendant: 'c = "descendant";',
|
||||
child: 'c = "child";',
|
||||
adjacent: 'c = "adjacent";',
|
||||
laterSibling: 'c = "laterSibling";'
|
||||
},
|
||||
|
||||
patterns: [
|
||||
// combinators must be listed first
|
||||
// (and descendant needs to be last combinator)
|
||||
{ name: 'laterSibling', re: /^\s*~\s*/ },
|
||||
{ name: 'child', re: /^\s*>\s*/ },
|
||||
{ name: 'adjacent', re: /^\s*\+\s*/ },
|
||||
{ name: 'descendant', re: /^\s/ },
|
||||
|
||||
// selectors follow
|
||||
{ name: 'tagName', re: /^\s*(\*|[\w\-]+)(\b|$)?/ },
|
||||
{ name: 'id', re: /^#([\w\-\*]+)(\b|$)/ },
|
||||
{ name: 'className', re: /^\.([\w\-\*]+)(\b|$)/ },
|
||||
{ name: 'pseudo', re: /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/ },
|
||||
{ name: 'attrPresence', re: /^\[((?:[\w-]+:)?[\w-]+)\]/ },
|
||||
{ name: 'attr', re: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/ }
|
||||
],
|
||||
|
||||
// for Prototype.LegacySelector.match and Element#match
|
||||
assertions: {
|
||||
tagName: function(element, matches) {
|
||||
return matches[1].toUpperCase() == element.tagName.toUpperCase();
|
||||
},
|
||||
|
||||
className: function(element, matches) {
|
||||
return Element.hasClassName(element, matches[1]);
|
||||
},
|
||||
|
||||
id: function(element, matches) {
|
||||
return element.id === matches[1];
|
||||
},
|
||||
|
||||
attrPresence: function(element, matches) {
|
||||
return Element.hasAttribute(element, matches[1]);
|
||||
},
|
||||
|
||||
attr: function(element, matches) {
|
||||
var nodeValue = Element.readAttribute(element, matches[1]);
|
||||
return nodeValue && Prototype.LegacySelector.operators[matches[2]](nodeValue, matches[5] || matches[6]);
|
||||
}
|
||||
},
|
||||
|
||||
handlers: {
|
||||
// UTILITY FUNCTIONS
|
||||
// joins two collections
|
||||
concat: function(a, b) {
|
||||
for (var i = 0, node; node = b[i]; i++)
|
||||
a.push(node);
|
||||
return a;
|
||||
},
|
||||
|
||||
// marks an array of nodes for counting
|
||||
mark: function(nodes) {
|
||||
var _true = Prototype.emptyFunction;
|
||||
for (var i = 0, node; node = nodes[i]; i++)
|
||||
node._countedByPrototype = _true;
|
||||
return nodes;
|
||||
},
|
||||
|
||||
unmark: (function(){
|
||||
|
||||
// IE improperly serializes _countedByPrototype in (inner|outer)HTML
|
||||
// due to node properties being mapped directly to attributes
|
||||
var PROPERTIES_ATTRIBUTES_MAP = (function(){
|
||||
var el = document.createElement('div'),
|
||||
isBuggy = false,
|
||||
propName = '_countedByPrototype',
|
||||
value = 'x';
|
||||
el[propName] = value;
|
||||
isBuggy = (el.getAttribute(propName) === value);
|
||||
el = null;
|
||||
return isBuggy;
|
||||
})();
|
||||
|
||||
return PROPERTIES_ATTRIBUTES_MAP ?
|
||||
function(nodes) {
|
||||
for (var i = 0, node; node = nodes[i]; i++)
|
||||
node.removeAttribute('_countedByPrototype');
|
||||
return nodes;
|
||||
} :
|
||||
function(nodes) {
|
||||
for (var i = 0, node; node = nodes[i]; i++)
|
||||
node._countedByPrototype = void 0;
|
||||
return nodes;
|
||||
}
|
||||
})(),
|
||||
|
||||
// mark each child node with its position (for nth calls)
|
||||
// "ofType" flag indicates whether we're indexing for nth-of-type
|
||||
// rather than nth-child
|
||||
index: function(parentNode, reverse, ofType) {
|
||||
parentNode._countedByPrototype = Prototype.emptyFunction;
|
||||
if (reverse) {
|
||||
for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {
|
||||
var node = nodes[i];
|
||||
if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
|
||||
}
|
||||
} else {
|
||||
for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)
|
||||
if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
|
||||
}
|
||||
},
|
||||
|
||||
// filters out duplicates and extends all nodes
|
||||
unique: function(nodes) {
|
||||
if (nodes.length == 0) return nodes;
|
||||
var results = [], n;
|
||||
for (var i = 0, l = nodes.length; i < l; i++)
|
||||
// use `typeof` operator to prevent errors
|
||||
if (typeof (n = nodes[i])._countedByPrototype == 'undefined') {
|
||||
n._countedByPrototype = Prototype.emptyFunction;
|
||||
results.push(Element.extend(n));
|
||||
}
|
||||
return Prototype.LegacySelector.handlers.unmark(results);
|
||||
},
|
||||
|
||||
// COMBINATOR FUNCTIONS
|
||||
descendant: function(nodes) {
|
||||
var h = Prototype.LegacySelector.handlers;
|
||||
for (var i = 0, results = [], node; node = nodes[i]; i++)
|
||||
h.concat(results, node.getElementsByTagName('*'));
|
||||
return results;
|
||||
},
|
||||
|
||||
child: function(nodes) {
|
||||
var h = Prototype.LegacySelector.handlers;
|
||||
for (var i = 0, results = [], node; node = nodes[i]; i++) {
|
||||
for (var j = 0, child; child = node.childNodes[j]; j++)
|
||||
if (child.nodeType == 1 && child.tagName != '!') results.push(child);
|
||||
}
|
||||
return results;
|
||||
},
|
||||
|
||||
adjacent: function(nodes) {
|
||||
for (var i = 0, results = [], node; node = nodes[i]; i++) {
|
||||
var next = this.nextElementSibling(node);
|
||||
if (next) results.push(next);
|
||||
}
|
||||
return results;
|
||||
},
|
||||
|
||||
laterSibling: function(nodes) {
|
||||
var h = Prototype.LegacySelector.handlers;
|
||||
for (var i = 0, results = [], node; node = nodes[i]; i++)
|
||||
h.concat(results, Element.nextSiblings(node));
|
||||
return results;
|
||||
},
|
||||
|
||||
nextElementSibling: function(node) {
|
||||
while (node = node.nextSibling)
|
||||
if (node.nodeType == 1) return node;
|
||||
return null;
|
||||
},
|
||||
|
||||
previousElementSibling: function(node) {
|
||||
while (node = node.previousSibling)
|
||||
if (node.nodeType == 1) return node;
|
||||
return null;
|
||||
},
|
||||
|
||||
// TOKEN FUNCTIONS
|
||||
tagName: function(nodes, root, tagName, combinator) {
|
||||
var uTagName = tagName.toUpperCase();
|
||||
var results = [], h = Prototype.LegacySelector.handlers;
|
||||
if (nodes) {
|
||||
if (combinator) {
|
||||
// fastlane for ordinary descendant combinators
|
||||
if (combinator == "descendant") {
|
||||
for (var i = 0, node; node = nodes[i]; i++)
|
||||
h.concat(results, node.getElementsByTagName(tagName));
|
||||
return results;
|
||||
} else nodes = this[combinator](nodes);
|
||||
if (tagName == "*") return nodes;
|
||||
}
|
||||
for (var i = 0, node; node = nodes[i]; i++)
|
||||
if (node.tagName.toUpperCase() === uTagName) results.push(node);
|
||||
return results;
|
||||
} else return root.getElementsByTagName(tagName);
|
||||
},
|
||||
|
||||
id: function(nodes, root, id, combinator) {
|
||||
var targetNode = $(id), h = Prototype.LegacySelector.handlers;
|
||||
|
||||
if (root == document) {
|
||||
// We don't have to deal with orphan nodes. Easy.
|
||||
if (!targetNode) return [];
|
||||
if (!nodes) return [targetNode];
|
||||
} else {
|
||||
// In IE, we can check sourceIndex to see if root is attached
|
||||
// to the document. If not (or if sourceIndex is not present),
|
||||
// we do it the hard way.
|
||||
if (!root.sourceIndex || root.sourceIndex < 1) {
|
||||
var nodes = root.getElementsByTagName('*');
|
||||
for (var j = 0, node; node = nodes[j]; j++) {
|
||||
if (node.id === id) return [node];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nodes) {
|
||||
if (combinator) {
|
||||
if (combinator == 'child') {
|
||||
for (var i = 0, node; node = nodes[i]; i++)
|
||||
if (targetNode.parentNode == node) return [targetNode];
|
||||
} else if (combinator == 'descendant') {
|
||||
for (var i = 0, node; node = nodes[i]; i++)
|
||||
if (Element.descendantOf(targetNode, node)) return [targetNode];
|
||||
} else if (combinator == 'adjacent') {
|
||||
for (var i = 0, node; node = nodes[i]; i++)
|
||||
if (Prototype.LegacySelector.handlers.previousElementSibling(targetNode) == node)
|
||||
return [targetNode];
|
||||
} else nodes = h[combinator](nodes);
|
||||
}
|
||||
for (var i = 0, node; node = nodes[i]; i++)
|
||||
if (node == targetNode) return [targetNode];
|
||||
return [];
|
||||
}
|
||||
return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : [];
|
||||
},
|
||||
|
||||
className: function(nodes, root, className, combinator) {
|
||||
if (nodes && combinator) nodes = this[combinator](nodes);
|
||||
return Prototype.LegacySelector.handlers.byClassName(nodes, root, className);
|
||||
},
|
||||
|
||||
byClassName: function(nodes, root, className) {
|
||||
if (!nodes) nodes = Prototype.LegacySelector.handlers.descendant([root]);
|
||||
var needle = ' ' + className + ' ';
|
||||
for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) {
|
||||
nodeClassName = node.className;
|
||||
if (nodeClassName.length == 0) continue;
|
||||
if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle))
|
||||
results.push(node);
|
||||
}
|
||||
return results;
|
||||
},
|
||||
|
||||
attrPresence: function(nodes, root, attr, combinator) {
|
||||
if (!nodes) nodes = root.getElementsByTagName("*");
|
||||
if (nodes && combinator) nodes = this[combinator](nodes);
|
||||
var results = [];
|
||||
for (var i = 0, node; node = nodes[i]; i++)
|
||||
if (Element.hasAttribute(node, attr)) results.push(node);
|
||||
return results;
|
||||
},
|
||||
|
||||
attr: function(nodes, root, attr, value, operator, combinator) {
|
||||
if (!nodes) nodes = root.getElementsByTagName("*");
|
||||
if (nodes && combinator) nodes = this[combinator](nodes);
|
||||
var handler = Prototype.LegacySelector.operators[operator], results = [];
|
||||
for (var i = 0, node; node = nodes[i]; i++) {
|
||||
var nodeValue = Element.readAttribute(node, attr);
|
||||
if (nodeValue === null) continue;
|
||||
if (handler(nodeValue, value)) results.push(node);
|
||||
}
|
||||
return results;
|
||||
},
|
||||
|
||||
pseudo: function(nodes, name, value, root, combinator) {
|
||||
if (nodes && combinator) nodes = this[combinator](nodes);
|
||||
if (!nodes) nodes = root.getElementsByTagName("*");
|
||||
return Prototype.LegacySelector.pseudos[name](nodes, value, root);
|
||||
}
|
||||
},
|
||||
|
||||
pseudos: {
|
||||
'first-child': function(nodes, value, root) {
|
||||
for (var i = 0, results = [], node; node = nodes[i]; i++) {
|
||||
if (Prototype.LegacySelector.handlers.previousElementSibling(node)) continue;
|
||||
results.push(node);
|
||||
}
|
||||
return results;
|
||||
},
|
||||
'last-child': function(nodes, value, root) {
|
||||
for (var i = 0, results = [], node; node = nodes[i]; i++) {
|
||||
if (Prototype.LegacySelector.handlers.nextElementSibling(node)) continue;
|
||||
results.push(node);
|
||||
}
|
||||
return results;
|
||||
},
|
||||
'only-child': function(nodes, value, root) {
|
||||
var h = Prototype.LegacySelector.handlers;
|
||||
for (var i = 0, results = [], node; node = nodes[i]; i++)
|
||||
if (!h.previousElementSibling(node) && !h.nextElementSibling(node))
|
||||
results.push(node);
|
||||
return results;
|
||||
},
|
||||
'nth-child': function(nodes, formula, root) {
|
||||
return Prototype.LegacySelector.pseudos.nth(nodes, formula, root);
|
||||
},
|
||||
'nth-last-child': function(nodes, formula, root) {
|
||||
return Prototype.LegacySelector.pseudos.nth(nodes, formula, root, true);
|
||||
},
|
||||
'nth-of-type': function(nodes, formula, root) {
|
||||
return Prototype.LegacySelector.pseudos.nth(nodes, formula, root, false, true);
|
||||
},
|
||||
'nth-last-of-type': function(nodes, formula, root) {
|
||||
return Prototype.LegacySelector.pseudos.nth(nodes, formula, root, true, true);
|
||||
},
|
||||
'first-of-type': function(nodes, formula, root) {
|
||||
return Prototype.LegacySelector.pseudos.nth(nodes, "1", root, false, true);
|
||||
},
|
||||
'last-of-type': function(nodes, formula, root) {
|
||||
return Prototype.LegacySelector.pseudos.nth(nodes, "1", root, true, true);
|
||||
},
|
||||
'only-of-type': function(nodes, formula, root) {
|
||||
var p = Prototype.LegacySelector.pseudos;
|
||||
return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root);
|
||||
},
|
||||
|
||||
// handles the an+b logic
|
||||
getIndices: function(a, b, total) {
|
||||
if (a == 0) return b > 0 ? [b] : [];
|
||||
return $R(1, total).inject([], function(memo, i) {
|
||||
if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i);
|
||||
return memo;
|
||||
});
|
||||
},
|
||||
|
||||
// handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type
|
||||
nth: function(nodes, formula, root, reverse, ofType) {
|
||||
if (nodes.length == 0) return [];
|
||||
if (formula == 'even') formula = '2n+0';
|
||||
if (formula == 'odd') formula = '2n+1';
|
||||
var h = Prototype.LegacySelector.handlers, results = [], indexed = [], m;
|
||||
h.mark(nodes);
|
||||
for (var i = 0, node; node = nodes[i]; i++) {
|
||||
if (!node.parentNode._countedByPrototype) {
|
||||
h.index(node.parentNode, reverse, ofType);
|
||||
indexed.push(node.parentNode);
|
||||
}
|
||||
}
|
||||
if (formula.match(/^\d+$/)) { // just a number
|
||||
formula = Number(formula);
|
||||
for (var i = 0, node; node = nodes[i]; i++)
|
||||
if (node.nodeIndex == formula) results.push(node);
|
||||
} else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
|
||||
if (m[1] == "-") m[1] = -1;
|
||||
var a = m[1] ? Number(m[1]) : 1;
|
||||
var b = m[2] ? Number(m[2]) : 0;
|
||||
var indices = Prototype.LegacySelector.pseudos.getIndices(a, b, nodes.length);
|
||||
for (var i = 0, node, l = indices.length; node = nodes[i]; i++) {
|
||||
for (var j = 0; j < l; j++)
|
||||
if (node.nodeIndex == indices[j]) results.push(node);
|
||||
}
|
||||
}
|
||||
h.unmark(nodes);
|
||||
h.unmark(indexed);
|
||||
return results;
|
||||
},
|
||||
|
||||
'empty': function(nodes, value, root) {
|
||||
for (var i = 0, results = [], node; node = nodes[i]; i++) {
|
||||
// IE treats comments as element nodes
|
||||
if (node.tagName == '!' || node.firstChild) continue;
|
||||
results.push(node);
|
||||
}
|
||||
return results;
|
||||
},
|
||||
|
||||
'not': function(nodes, selector, root) {
|
||||
var h = Prototype.LegacySelector.handlers, selectorType, m;
|
||||
var exclusions = new Prototype.LegacySelector(selector).findElements(root);
|
||||
h.mark(exclusions);
|
||||
for (var i = 0, results = [], node; node = nodes[i]; i++)
|
||||
if (!node._countedByPrototype) results.push(node);
|
||||
h.unmark(exclusions);
|
||||
return results;
|
||||
},
|
||||
|
||||
'enabled': function(nodes, value, root) {
|
||||
for (var i = 0, results = [], node; node = nodes[i]; i++)
|
||||
if (!node.disabled && (!node.type || node.type !== 'hidden'))
|
||||
results.push(node);
|
||||
return results;
|
||||
},
|
||||
|
||||
'disabled': function(nodes, value, root) {
|
||||
for (var i = 0, results = [], node; node = nodes[i]; i++)
|
||||
if (node.disabled) results.push(node);
|
||||
return results;
|
||||
},
|
||||
|
||||
'checked': function(nodes, value, root) {
|
||||
for (var i = 0, results = [], node; node = nodes[i]; i++)
|
||||
if (node.checked) results.push(node);
|
||||
return results;
|
||||
}
|
||||
},
|
||||
|
||||
operators: {
|
||||
'=': function(nv, v) { return nv == v; },
|
||||
'!=': function(nv, v) { return nv != v; },
|
||||
'^=': function(nv, v) { return nv == v || nv && nv.startsWith(v); },
|
||||
'$=': function(nv, v) { return nv == v || nv && nv.endsWith(v); },
|
||||
'*=': function(nv, v) { return nv == v || nv && nv.include(v); },
|
||||
'~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); },
|
||||
'|=': function(nv, v) { return ('-' + (nv || "").toUpperCase() +
|
||||
'-').include('-' + (v || "").toUpperCase() + '-'); }
|
||||
},
|
||||
|
||||
split: function(expression) {
|
||||
var expressions = [];
|
||||
expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
|
||||
expressions.push(m[1].strip());
|
||||
});
|
||||
return expressions;
|
||||
},
|
||||
|
||||
matchElements: function(elements, expression) {
|
||||
var matches = $$(expression), h = Prototype.LegacySelector.handlers;
|
||||
h.mark(matches);
|
||||
for (var i = 0, results = [], element; element = elements[i]; i++)
|
||||
if (element._countedByPrototype) results.push(element);
|
||||
h.unmark(matches);
|
||||
return results;
|
||||
},
|
||||
|
||||
findElement: function(elements, expression, index) {
|
||||
if (Object.isNumber(expression)) {
|
||||
index = expression; expression = false;
|
||||
}
|
||||
return Prototype.LegacySelector.matchElements(elements, expression || '*')[index || 0];
|
||||
},
|
||||
|
||||
findChildElements: function(element, expressions) {
|
||||
expressions = Prototype.LegacySelector.split(expressions.join(','));
|
||||
var results = [], h = Prototype.LegacySelector.handlers;
|
||||
for (var i = 0, l = expressions.length, selector; i < l; i++) {
|
||||
selector = new Prototype.LegacySelector(expressions[i].strip());
|
||||
h.concat(results, selector.findElements(element));
|
||||
}
|
||||
return (l > 1) ? h.unique(results) : results;
|
||||
}
|
||||
});
|
||||
|
||||
if (Prototype.Browser.IE) {
|
||||
Object.extend(Prototype.LegacySelector.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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
//= require "repository/legacy_selector"
|
||||
|
||||
Prototype.Selector = (function(engine) {
|
||||
function select(selector, scope) {
|
||||
return engine.findChildElements(scope || document, [selector]);
|
||||
}
|
||||
|
||||
function match(element, selector) {
|
||||
return !!engine.findElement([element], selector);
|
||||
}
|
||||
|
||||
return {
|
||||
engine: engine,
|
||||
select: select,
|
||||
match: match,
|
||||
filter: engine.matchElements
|
||||
};
|
||||
})(Prototype.LegacySelector);
|
|
@ -0,0 +1 @@
|
|||
Subproject commit c9f5d5d4fc4ca294477f803bb8d688a8d45de664
|
|
@ -0,0 +1,18 @@
|
|||
Prototype._original_property = window.NW;
|
||||
//= require "repository/src/nwmatcher"
|
||||
|
||||
Prototype.Selector = (function(engine) {
|
||||
function select(selector, scope) {
|
||||
return engine.select(selector, scope || document, null, Element.extend);
|
||||
}
|
||||
|
||||
return {
|
||||
engine: engine,
|
||||
select: select,
|
||||
match: engine.match
|
||||
};
|
||||
})(NW.Dom);
|
||||
|
||||
// Restore globals.
|
||||
window.NW = Prototype._original_property;
|
||||
delete Prototype._original_property;
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 472a55dd0019acf034d4f72522915a5e9efd0a1a
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 415e466f70e5a53f589161b1f2944e5485007409
|
|
@ -0,0 +1,34 @@
|
|||
Prototype._original_property = window.Sizzle;
|
||||
//= require "repository/sizzle"
|
||||
|
||||
Prototype.Selector = (function(engine) {
|
||||
function extend(elements) {
|
||||
for (var i = 0, length = elements.length; i < length; i++) {
|
||||
Element.extend(elements[i]);
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
|
||||
function select(selector, scope) {
|
||||
return extend(engine(selector, scope || document));
|
||||
}
|
||||
|
||||
function match(element, selector) {
|
||||
return engine.matches(selector, [element]).length == 1;
|
||||
}
|
||||
|
||||
function filter(elements, selector) {
|
||||
return extend(engine.matches(selector, elements));
|
||||
}
|
||||
|
||||
return {
|
||||
engine: engine,
|
||||
select: select,
|
||||
match: match,
|
||||
filter: filter
|
||||
};
|
||||
})(Sizzle);
|
||||
|
||||
// Restore globals.
|
||||
window.Sizzle = Prototype._original_property;
|
||||
delete Prototype._original_property;
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 8860b7f54d08dd4861bea1ca5de223555ad4b2c1
|
|
@ -0,0 +1 @@
|
|||
Subproject commit b81172275ea4a6530a5bac4912f2f18ecbfe9feb
|
Loading…
Reference in New Issue