├── .gitignore
├── MIT-LICENSE
├── README.markdown
├── TODO
├── example
├── example.html
├── images
│ ├── closebox.png
│ ├── closebox_selected.png
│ ├── end.png
│ ├── start.png
│ └── startend.png
├── scripts
│ └── prototype.js
└── stylesheets
│ ├── gui.css
│ ├── ie6.css
│ └── timeframe.css
├── test
├── index.html
├── unittest.css
└── unittest.js
└── timeframe.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2008 Stephen Celis
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.markdown:
--------------------------------------------------------------------------------
1 | Timeframe
2 | =========
3 |
4 | Click-draggable. Range-makeable. A better calendar.
5 |
6 |
7 | The code:
8 | ---------
9 |
10 | new Timeframe(element, options);
11 |
12 |
13 | ### Options available:
14 |
15 | * `months`:
16 | The number of calendar months showing at once (default: `2`).
17 |
18 | * `format`:
19 | The strftime format for the dates in the input fields (default:
20 | `%b %d, %Y`). (With [Datejs](http://datejs.com), it takes Datejs
21 | formatting.)
22 |
23 | * `weekOffset`:
24 | Override the localization's default weekday start with this option (e.g.,
25 | `1` will force the rows to start on Monday; use `0` for Sunday).
26 |
27 | * `startField`, `endField`:
28 | Declare the range start and end input tags (by default, these are generated
29 | with the Timeframe). When the `value` attribute is pre-populated, the
30 | Timeframe will load with this range.
31 |
32 | * `previousButton`, `todayButton`, `nextButton`, `resetButton`:
33 | Declare the navigational buttons (these are also generated by default with
34 | the Timeframe).
35 |
36 | * `earliest`, `latest`:
37 | The earliest and latest selectable dates (accepts either a `Date` object or
38 | a `String` that can be parsed with `Date.parse()`).
39 |
40 | * `maxRange`:
41 | Limit the maximum possible range length (set to `1` to turn Timeframe into
42 | a regular old date picker).
43 |
44 |
45 | ### Localization:
46 |
47 | Drop in a localized version of [Datejs](http://datejs.com), and it should just
48 | work. An added bonus is that the text fields will live-parse more nicely! Just
49 | try "next tues."
50 |
51 |
52 | ### Notes:
53 |
54 | * I'm just sick of multiple date pickers on the same page.
55 |
56 |
57 | An example:
58 | -----------
59 |
60 |
69 |
70 | See it in action
71 | [here](http://stephencelis.com/projects/timeframe#example_information).
72 |
73 | Dependencies:
74 | -------------
75 |
76 | Timeframe requires [Prototype](http://prototypejs.org) 1.6 or higher.
77 |
78 |
79 | Contributors:
80 | -------------
81 |
82 | * Justin Palmer ("Caged")
83 | * Nik Wakelin ("codetocustomer")
84 | * Sebastien Grosjean ("ZenCocoon")
85 | * Will Bryant ("willbryant")
86 |
87 |
88 | Download:
89 | ---------
90 |
91 | Find the latest version of Timeframe on
92 | [Github](http://github.com/stephencelis/timeframe).
93 |
94 | More information can be found
95 | [here](http://stephencelis.com/projects/timeframe).
96 |
97 |
98 | Copyright (c) 2008-2011 [Stephen Celis](http://stephencelis.com), released under
99 | the [MIT license](http://en.wikipedia.org/wiki/Mit_license).
--------------------------------------------------------------------------------
/TODO:
--------------------------------------------------------------------------------
1 | THINGS TODO (mostly in order of importance)
2 |
3 | * Optimize for IE
4 |
5 | * More localizations
6 |
7 | * Prettier initial styles, especially for non-Safari browsers
8 | - Just get the reset button generation out of there
9 | - Maybe do hidden inputs for the generated field defaults
10 |
11 | * iPhone optimization
12 |
13 | * Never too late for code CLEANUP and optimizations
14 |
15 | * [?] Range options: minrange, defaultrange (do minrange and defaultrange
16 | make sense?)
--------------------------------------------------------------------------------
/example/example.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 | Timeframe
7 |
8 |
9 |
10 |
17 |
20 |
21 |
22 |
23 |
24 |
25 | Click-draggable. Range-makeable. A better calendar.
26 |
27 |
28 |
29 | The code:
30 |
31 |
32 | new Timeframe(element, options);
33 |
34 |
35 | Options available:
36 |
37 |
38 | months
39 | The number of calendar months showing at once (default: 2
).
40 | format
41 | The strftime format for the dates in the input fields (default: %b %d, %Y
). (With Datejs , it takes Datejs formatting.)
42 | weekOffset
43 | The weekday offset (use 1
to start the week on Monday).
44 | startField
, endField
45 | Declare the range start and end input
tags (by default, these are generated with the Timeframe).
46 | previousButton
, todayButton
, nextButton
, resetButton
47 | Declare the navigational buttons (by default, these are also generated with the Timeframe).
48 | earliest
, latest
49 | The earliest and latest selectable dates (accepts either a Date
object or a String
that can be parsed with Date.parse()
).
50 | maxRange
51 | Limit the maximum possible range length (set to 1
to turn Timeframe into a regular old date picker).
52 |
53 |
Localization:
54 |
55 | Drop in a localized version of Datejs , and it should just work. An added bonus is that the text fields will live-parse more nicely! Just try “next tues.”
56 |
57 |
58 | Notes:
59 |
60 |
61 | I’m just sick of multiple date pickers on the same page.
62 |
63 |
66 |
67 | A little bit slow in IE. Optimizations forthcoming.
68 | Try Safari for the best experience.
69 |
70 |
71 |
Please select a date range below:
72 |
73 |
85 |
86 |
87 | Generated from this code (see the source for more detail):
88 |
89 |
90 | <script type="text/javascript" charset="utf-8">
91 | //<![CDATA[
92 | new Timeframe('calendars', {
93 | startField: 'start',
94 | endField: 'end',
95 | earliest: new Date(),
96 | resetButton: 'reset' });
97 | //]]>
98 | </script>
99 |
100 |
Contribution:
101 |
102 | Timeframe is open source and available for forking, pushing, and pulling at Github :
103 |
104 |
105 | http://github.com/stephencelis/timeframe
106 |
107 |
108 | If you find Timeframe useful, please feel free to leave a donation:
109 |
110 |
111 |
Thanks:
112 |
117 |
Contact:
118 |
119 | Contact me with questions/comments at
120 | .
126 |
127 |
128 | Learn more about me at stephencelis.com .
129 |
130 |
131 | Copyright © 2008 Stephen Celis. Provided under the MIT License.
132 |
133 |
134 |
135 |
136 |
145 |
146 |
--------------------------------------------------------------------------------
/example/images/closebox.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stephencelis/timeframe/368dad8ffae0bb0246ca34a11a16f7a681b6b210/example/images/closebox.png
--------------------------------------------------------------------------------
/example/images/closebox_selected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stephencelis/timeframe/368dad8ffae0bb0246ca34a11a16f7a681b6b210/example/images/closebox_selected.png
--------------------------------------------------------------------------------
/example/images/end.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stephencelis/timeframe/368dad8ffae0bb0246ca34a11a16f7a681b6b210/example/images/end.png
--------------------------------------------------------------------------------
/example/images/start.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stephencelis/timeframe/368dad8ffae0bb0246ca34a11a16f7a681b6b210/example/images/start.png
--------------------------------------------------------------------------------
/example/images/startend.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stephencelis/timeframe/368dad8ffae0bb0246ca34a11a16f7a681b6b210/example/images/startend.png
--------------------------------------------------------------------------------
/example/stylesheets/gui.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: Arial, Helvetica, sans-serif;
3 | text-shadow: 0 0 0 #fff;
4 | margin: 0;
5 | }
6 | #page_container {
7 | margin: auto;
8 | width: 600px;
9 | }
10 | p {
11 | line-height: 1.4;
12 | }
13 | dd {
14 | padding: 0 0 10px;
15 | }
16 | #example {
17 | padding: 10px;
18 | padding: 5px 128px 40px;
19 | background: #eee;
20 | border: 3px solid #fff;
21 |
22 | box-shadow: 0 1px 10px #999;
23 | -webkit-box-shadow: 0 1px 10px #999;
24 | -moz-box-shadow: 0 1px 10px #999;
25 | border-radius: 6px;
26 | -webkit-border-radius: 6px;
27 | -moz-border-radius: 6px;
28 | }
29 |
30 | /* Form */
31 |
32 | #calendar_form {
33 | clear: both;
34 | }
35 | #calendar_form #labels label {
36 | font-weight: bold;
37 | }
38 | #calendar_form #fields span {
39 | background: #ccc;
40 | border-bottom: 1px solid #999;
41 | display: inline-block;
42 | padding: 0 4px;
43 |
44 | box-shadow: 0 1px 1px #ccc;
45 | -webkit-box-shadow: 0 1px 1px #ccc;
46 | -moz-box-shadow: 0 1px 1px #ccc;
47 | }
48 | #calendar_form #fields span input {
49 | border-style: none;
50 | background-color: transparent;
51 | font-size: 14px;
52 | padding: 2px 6px;
53 | width: 96px;
54 |
55 | border-radius: 10px;
56 | -webkit-border-radius: 10px;
57 | -moz-border-radius: 11px;
58 | }
59 | #calendar_form #fields span input:focus {
60 | outline: none;
61 | background: #aaa;
62 | }
63 | #calendar_form #fields span input.error {
64 | background: #faa;
65 | }
--------------------------------------------------------------------------------
/example/stylesheets/ie6.css:
--------------------------------------------------------------------------------
1 | div.timeframe_calendar li a, div.timeframe_calendar table {
2 | display: inline;
3 | }
4 | div.timeframe_calendar tbody td.startrange, div.timeframe_calendar tbody td.endrange, div.timeframe_calendar tbody td.startendrange, div.timeframe_calendar tbody td span.clear span, div.timeframe_calendar tbody td span.clear span.active {
5 | background-image: none;
6 | }
7 | div.timeframe_calendar tbody td.startrange {
8 | filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='images/start.png', sizingMethod='crop');
9 | }
10 | div.timeframe_calendar tbody td.endrange {
11 | filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='images/end.png', sizingMethod='crop');
12 | }
13 | div.timeframe_calendar tbody td.startendrange {
14 | filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='images/startend.png', sizingMethod='crop');
15 | }
16 | div.timeframe_calendar tbody td span.clear span {
17 | filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='images/closebox.png', sizingMethod='crop');
18 | }
19 | div.timeframe_calendar tbody td span.clear span.active {
20 | filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='images/closebox_selected.png', sizingMethod='crop');
21 | }
22 | div.timeframe_calendar tbody td.beyond {
23 | filter: none;
24 | }
--------------------------------------------------------------------------------
/example/stylesheets/timeframe.css:
--------------------------------------------------------------------------------
1 | div.timeframe_calendar {
2 | display: inline-block;
3 | margin: 0;
4 | padding: 0;
5 | text-align: center;
6 | text-shadow: none;
7 | }
8 |
9 | /* Menu */
10 | div.timeframe_calendar ul.timeframe_menu {
11 | list-style-type: none;
12 | font-weight: bold;
13 | margin: auto;
14 | padding: 0 0 6px;
15 | width: 60px;
16 | }
17 | div.timeframe_calendar ul.timeframe_menu li {
18 | display: inline;
19 | }
20 | div.timeframe_calendar ul.timeframe_menu li a {
21 | display: inline-block;
22 | height: 20px;
23 | padding: 2px 0 0;
24 | text-decoration: none;
25 | width: 20px;
26 | box-shadow: 0 1px 2px #999;
27 | -webkit-box-shadow: 0 1px 2px #999;
28 | -moz-box-shadow: 0 1px 2px #999;
29 | }
30 | div.timeframe_calendar ul.timeframe_menu li a.previous, div.timeframe_calendar ul.timeframe_menu li a.next {
31 | background: #fff;
32 | color: #468966;
33 | }
34 | div.timeframe_calendar ul.timeframe_menu li a.previous:hover, div.timeframe_calendar ul.timeframe_menu li a.next:hover {
35 | background: #ccc;
36 | }
37 | div.timeframe_calendar ul.timeframe_menu li a.previous:active, div.timeframe_calendar ul.timeframe_menu li a.next:active {
38 | background: #aaa;
39 | }
40 | div.timeframe_calendar ul.timeframe_menu li a.disabled, div.timeframe_calendar ul.timeframe_menu li a.disabled:hover, div.timeframe_calendar ul.timeframe_menu li a.disabled:active {
41 | background: #fff;
42 | color: #ccc;
43 | cursor: default;
44 | }
45 | div.timeframe_calendar ul.timeframe_menu li a.today {
46 | background: #468966;
47 | color: #eee;
48 | }
49 | div.timeframe_calendar ul.timeframe_menu li a.today:hover {
50 | background: #246744;
51 | }
52 | div.timeframe_calendar ul.timeframe_menu li a.today:active {
53 | background: #024522;
54 | }
55 | div.timeframe_calendar ul.timeframe_menu li a.previous {
56 | border-top-left-radius: 10px;
57 | border-bottom-left-radius: 10px;
58 | -webkit-border-top-left-radius: 10px;
59 | -webkit-border-bottom-left-radius: 10px;
60 | -moz-border-radius-topleft: 11px;
61 | -moz-border-radius-bottomleft: 11px;
62 | }
63 | div.timeframe_calendar ul.timeframe_menu li a.next {
64 | border-top-right-radius: 10px;
65 | border-bottom-right-radius: 10px;
66 | -webkit-border-top-right-radius: 10px;
67 | -webkit-border-bottom-right-radius: 10px;
68 | -moz-border-radius-topright: 11px;
69 | -moz-border-radius-bottomright: 11px;
70 | }
71 |
72 | /* Calendar*/
73 | div.timeframe_calendar table {
74 | border-collapse: collapse;
75 | display: inline;
76 | display: inline-block;
77 | font-size: 15px;
78 | margin: 0 6px 12px;
79 | }
80 | /* Month names */
81 | div.timeframe_calendar table caption {
82 | text-shadow: 0 0 0 #fff;
83 | }
84 | /* Cell sizes */
85 | div.timeframe_calendar thead th, div.timeframe_calendar tbody td {
86 | height: 18px;
87 | margin: 0;
88 | padding: 2px 1px;
89 | width: 20px;
90 | }
91 | /* Weekday letters */
92 | div.timeframe_calendar thead {
93 | background: #222;
94 | color: #eee;
95 | }
96 | /* Days */
97 | div.timeframe_calendar tbody {
98 | background: #fff;
99 | box-shadow: 0px 2px 6px #999;
100 | -webkit-box-shadow: 0px 2px 6px #999;
101 | -moz-box-shadow: 0px 2px 6px #999;
102 | }
103 | div.timeframe_calendar tbody td {
104 | cursor: pointer;
105 | }
106 | /* Hover states not available in IE */
107 | div.timeframe_calendar tbody td.selectable:hover {
108 | background-color: #bbb;
109 | }
110 | div.timeframe_calendar tbody td.selected:hover, div.timeframe_calendar tbody td.stuck:hover {
111 | background-color: #e99a27;
112 | }
113 | /* Selected states */
114 | div.timeframe_calendar tbody td.selected {
115 | background-color: #ffb03b;
116 | }
117 | div.timeframe_calendar tbody td.stuck {
118 | background-color: #e99a27;
119 | }
120 | /* Range markers */
121 | div.timeframe_calendar tbody td.startrange, div.timeframe_calendar tbody td.endrange, div.timeframe_calendar tbody td.startendrange {
122 | cursor: col-resize;
123 | }
124 | div.timeframe_calendar tbody td.startrange {
125 | background-image: url(../images/start.png);
126 | }
127 | div.timeframe_calendar tbody td.endrange {
128 | background-image: url(../images/end.png);
129 | }
130 | div.timeframe_calendar tbody td.startendrange {
131 | background-image: url(../images/startend.png);
132 | }
133 | /* Today */
134 | div.timeframe_calendar tbody td.today {
135 | background-color: #468966;
136 | color: #eee;
137 | }
138 | div.timeframe_calendar tbody td.today_selected {
139 | background-color: #b64926;
140 | }
141 | div.timeframe_calendar tbody td.today_stuck {
142 | background-color: #8e2800;
143 | }
144 | /* Post/pre-month */
145 | div.timeframe_calendar tbody td.beyond {
146 | background-color: #aaa;
147 | background-image: none;
148 | color: #ccc;
149 | }
150 | div.timeframe_calendar tbody td.beyond_selected {
151 | background-color: #999;
152 | }
153 | div.timeframe_calendar tbody td.beyond_stuck {
154 | background-color: #888;
155 | }
156 |
157 | div.timeframe_calendar tbody td.unselectable {
158 | color: #ccc;
159 | cursor: default;
160 | }
161 | /* Clear button */
162 | div.timeframe_calendar tbody td span.clear {
163 | color: transparent;
164 | display: block;
165 | height: 0;
166 | position: absolute;
167 | width: 0;
168 | }
169 | div.timeframe_calendar tbody td span.clear span {
170 | background-image: url(../images/closebox.png);
171 | cursor: pointer;
172 | display: block;
173 | height: 30px;
174 | left: -18px;
175 | position: relative;
176 | text-indent: -10000px;
177 | top: -18px;
178 | width: 30px;
179 | }
180 | div.timeframe_calendar tbody td span.clear span.active {
181 | background-image: url(../images/closebox_selected.png);
182 | }
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 | Timeframe unit test file
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
528 |
529 |
--------------------------------------------------------------------------------
/test/unittest.css:
--------------------------------------------------------------------------------
1 | body, div, p, h1, h2, h3, ul, ol, span, a, table, td, form, img, li {
2 | font-family: sans-serif;
3 | }
4 |
5 | body {
6 | font-size:0.8em;
7 | }
8 |
9 | .navigation {
10 | background: #9DC569;
11 | color: #fff;
12 | }
13 |
14 | .navigation h1 {
15 | font-size: 20px;
16 | }
17 |
18 | .navigation h2 {
19 | font-size: 16px;
20 | font-weight: normal;
21 | margin: 0;
22 | border: 1px solid #e8a400;
23 | border-bottom: 0;
24 | background: #ffc;
25 | color: #E8A400;
26 | padding: 8px;
27 | padding-bottom: 0;
28 | }
29 |
30 | .navigation ul {
31 | margin-top: 0;
32 | border: 1px solid #E8A400;
33 | border-top: none;
34 | background: #ffc;
35 | padding: 8px;
36 | margin-left: 0;
37 | }
38 |
39 | .navigation ul li {
40 | font-size: 12px;
41 | list-style-type: none;
42 | margin-top: 1px;
43 | margin-bottom: 1px;
44 | color: #555;
45 | }
46 |
47 | .navigation a {
48 | color: #ffc;
49 | }
50 |
51 | .navigation ul li a {
52 | color: #000;
53 | }
54 |
55 | #log {
56 | padding-bottom: 1em;
57 | border-bottom: 2px solid #000;
58 | margin-bottom: 2em;
59 | }
60 |
61 | #logsummary {
62 | margin-bottom: 1em;
63 | padding: 1ex;
64 | border: 1px solid #000;
65 | font-weight: bold;
66 | }
67 |
68 | #logtable {
69 | width:100%;
70 | border-collapse: collapse;
71 | border: 1px dotted #666;
72 | }
73 |
74 | #logtable td, #logtable th {
75 | text-align: left;
76 | padding: 3px 8px;
77 | border: 1px dotted #666;
78 | }
79 |
80 | #logtable .passed {
81 | background-color: #cfc;
82 | }
83 |
84 | #logtable .failed, #logtable .error {
85 | background-color: #fcc;
86 | }
87 |
88 | #logtable .nameCell {
89 | cursor: pointer;
90 | }
--------------------------------------------------------------------------------
/test/unittest.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
2 | // (c) 2005-2008 Jon Tirsen (http://www.tirsen.com)
3 | // (c) 2005-2008 Michael Schuerig (http://www.schuerig.de/michael/)
4 | //
5 | // script.aculo.us is freely distributable under the terms of an MIT-style license.
6 | // For details, see the script.aculo.us web site: http://script.aculo.us/
7 |
8 | // experimental, Firefox-only
9 | Event.simulateMouse = function(element, eventName) {
10 | var options = Object.extend({
11 | pointerX: 0,
12 | pointerY: 0,
13 | buttons: 0,
14 | ctrlKey: false,
15 | altKey: false,
16 | shiftKey: false,
17 | metaKey: false
18 | }, arguments[2] || {});
19 | var oEvent = document.createEvent("MouseEvents");
20 | oEvent.initMouseEvent(eventName, true, true, document.defaultView,
21 | options.buttons, options.pointerX, options.pointerY, options.pointerX, options.pointerY,
22 | options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, 0, $(element));
23 |
24 | if(this.mark) Element.remove(this.mark);
25 | this.mark = document.createElement('div');
26 | this.mark.appendChild(document.createTextNode(" "));
27 | document.body.appendChild(this.mark);
28 | this.mark.style.position = 'absolute';
29 | this.mark.style.top = options.pointerY + "px";
30 | this.mark.style.left = options.pointerX + "px";
31 | this.mark.style.width = "5px";
32 | this.mark.style.height = "5px;";
33 | this.mark.style.borderTop = "1px solid red;";
34 | this.mark.style.borderLeft = "1px solid red;";
35 |
36 | if(this.step)
37 | alert('['+new Date().getTime().toString()+'] '+eventName+'/'+Test.Unit.inspect(options));
38 |
39 | $(element).dispatchEvent(oEvent);
40 | };
41 |
42 | // 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.
43 | // You need to downgrade to 1.0.4 for now to get this working
44 | // See https://bugzilla.mozilla.org/show_bug.cgi?id=289940 for the fix that fixed too much
45 | Event.simulateKey = function(element, eventName) {
46 | var options = Object.extend({
47 | ctrlKey: false,
48 | altKey: false,
49 | shiftKey: false,
50 | metaKey: false,
51 | keyCode: 0,
52 | charCode: 0
53 | }, arguments[2] || {});
54 |
55 | var oEvent = document.createEvent("KeyEvents");
56 | oEvent.initKeyEvent(eventName, true, true, window,
57 | options.ctrlKey, options.altKey, options.shiftKey, options.metaKey,
58 | options.keyCode, options.charCode );
59 | $(element).dispatchEvent(oEvent);
60 | };
61 |
62 | Event.simulateKeys = function(element, command) {
63 | for(var i=0; i' +
114 | '' +
115 | 'Status Test Message ' +
116 | ' ' +
117 | '
';
118 | this.logsummary = $('logsummary');
119 | this.loglines = $('loglines');
120 | },
121 | _toHTML: function(txt) {
122 | return txt.escapeHTML().replace(/\n/g," ");
123 | },
124 | addLinksToResults: function(){
125 | $$("tr.failed .nameCell").each( function(td){ // todo: limit to children of this.log
126 | td.title = "Run only this test";
127 | Event.observe(td, 'click', function(){ window.location.search = "?tests=" + td.innerHTML;});
128 | });
129 | $$("tr.passed .nameCell").each( function(td){ // todo: limit to children of this.log
130 | td.title = "Run all tests";
131 | Event.observe(td, 'click', function(){ window.location.search = "";});
132 | });
133 | }
134 | };
135 |
136 | Test.Unit.Runner = Class.create();
137 | Test.Unit.Runner.prototype = {
138 | initialize: function(testcases) {
139 | this.options = Object.extend({
140 | testLog: 'testlog'
141 | }, arguments[1] || {});
142 | this.options.resultsURL = this.parseResultsURLQueryParameter();
143 | this.options.tests = this.parseTestsQueryParameter();
144 | if (this.options.testLog) {
145 | this.options.testLog = $(this.options.testLog) || null;
146 | }
147 | if(this.options.tests) {
148 | this.tests = [];
149 | for(var i = 0; i < this.options.tests.length; i++) {
150 | if(/^test/.test(this.options.tests[i])) {
151 | this.tests.push(new Test.Unit.Testcase(this.options.tests[i], testcases[this.options.tests[i]], testcases["setup"], testcases["teardown"]));
152 | }
153 | }
154 | } else {
155 | if (this.options.test) {
156 | this.tests = [new Test.Unit.Testcase(this.options.test, testcases[this.options.test], testcases["setup"], testcases["teardown"])];
157 | } else {
158 | this.tests = [];
159 | for(var testcase in testcases) {
160 | if(/^test/.test(testcase)) {
161 | this.tests.push(
162 | new Test.Unit.Testcase(
163 | this.options.context ? ' -> ' + this.options.titles[testcase] : testcase,
164 | testcases[testcase], testcases["setup"], testcases["teardown"]
165 | ));
166 | }
167 | }
168 | }
169 | }
170 | this.currentTest = 0;
171 | this.logger = new Test.Unit.Logger(this.options.testLog);
172 | setTimeout(this.runTests.bind(this), 1000);
173 | },
174 | parseResultsURLQueryParameter: function() {
175 | return window.location.search.parseQuery()["resultsURL"];
176 | },
177 | parseTestsQueryParameter: function(){
178 | if (window.location.search.parseQuery()["tests"]){
179 | return window.location.search.parseQuery()["tests"].split(',');
180 | };
181 | },
182 | // Returns:
183 | // "ERROR" if there was an error,
184 | // "FAILURE" if there was a failure, or
185 | // "SUCCESS" if there was neither
186 | getResult: function() {
187 | var hasFailure = false;
188 | for(var i=0;i 0) {
190 | return "ERROR";
191 | }
192 | if (this.tests[i].failures > 0) {
193 | hasFailure = true;
194 | }
195 | }
196 | if (hasFailure) {
197 | return "FAILURE";
198 | } else {
199 | return "SUCCESS";
200 | }
201 | },
202 | postResults: function() {
203 | if (this.options.resultsURL) {
204 | new Ajax.Request(this.options.resultsURL,
205 | { method: 'get', parameters: 'result=' + this.getResult(), asynchronous: false });
206 | }
207 | },
208 | runTests: function() {
209 | var test = this.tests[this.currentTest];
210 | if (!test) {
211 | // finished!
212 | this.postResults();
213 | this.logger.summary(this.summary());
214 | return;
215 | }
216 | if(!test.isWaiting) {
217 | this.logger.start(test.name);
218 | }
219 | test.run();
220 | if(test.isWaiting) {
221 | this.logger.message("Waiting for " + test.timeToWait + "ms");
222 | setTimeout(this.runTests.bind(this), test.timeToWait || 1000);
223 | } else {
224 | this.logger.finish(test.status(), test.summary());
225 | this.currentTest++;
226 | // tail recursive, hopefully the browser will skip the stackframe
227 | this.runTests();
228 | }
229 | },
230 | summary: function() {
231 | var assertions = 0;
232 | var failures = 0;
233 | var errors = 0;
234 | var messages = [];
235 | for(var i=0;i 0) return 'failed';
280 | if (this.errors > 0) return 'error';
281 | return 'passed';
282 | },
283 | assert: function(expression) {
284 | var message = arguments[1] || 'assert: got "' + Test.Unit.inspect(expression) + '"';
285 | try { expression ? this.pass() :
286 | this.fail(message); }
287 | catch(e) { this.error(e); }
288 | },
289 | assertEqual: function(expected, actual) {
290 | var message = arguments[2] || "assertEqual";
291 | try { (expected == actual) ? this.pass() :
292 | this.fail(message + ': expected "' + Test.Unit.inspect(expected) +
293 | '", actual "' + Test.Unit.inspect(actual) + '"'); }
294 | catch(e) { this.error(e); }
295 | },
296 | assertInspect: function(expected, actual) {
297 | var message = arguments[2] || "assertInspect";
298 | try { (expected == actual.inspect()) ? this.pass() :
299 | this.fail(message + ': expected "' + Test.Unit.inspect(expected) +
300 | '", actual "' + Test.Unit.inspect(actual) + '"'); }
301 | catch(e) { this.error(e); }
302 | },
303 | assertEnumEqual: function(expected, actual) {
304 | var message = arguments[2] || "assertEnumEqual";
305 | try { $A(expected).length == $A(actual).length &&
306 | expected.zip(actual).all(function(pair) { return pair[0] == pair[1] }) ?
307 | this.pass() : this.fail(message + ': expected ' + Test.Unit.inspect(expected) +
308 | ', actual ' + Test.Unit.inspect(actual)); }
309 | catch(e) { this.error(e); }
310 | },
311 | assertNotEqual: function(expected, actual) {
312 | var message = arguments[2] || "assertNotEqual";
313 | try { (expected != actual) ? this.pass() :
314 | this.fail(message + ': got "' + Test.Unit.inspect(actual) + '"'); }
315 | catch(e) { this.error(e); }
316 | },
317 | assertIdentical: function(expected, actual) {
318 | var message = arguments[2] || "assertIdentical";
319 | try { (expected === actual) ? this.pass() :
320 | this.fail(message + ': expected "' + Test.Unit.inspect(expected) +
321 | '", actual "' + Test.Unit.inspect(actual) + '"'); }
322 | catch(e) { this.error(e); }
323 | },
324 | assertNotIdentical: function(expected, actual) {
325 | var message = arguments[2] || "assertNotIdentical";
326 | try { !(expected === actual) ? this.pass() :
327 | this.fail(message + ': expected "' + Test.Unit.inspect(expected) +
328 | '", actual "' + Test.Unit.inspect(actual) + '"'); }
329 | catch(e) { this.error(e); }
330 | },
331 | assertNull: function(obj) {
332 | var message = arguments[1] || 'assertNull';
333 | try { (obj==null) ? this.pass() :
334 | this.fail(message + ': got "' + Test.Unit.inspect(obj) + '"'); }
335 | catch(e) { this.error(e); }
336 | },
337 | assertMatch: function(expected, actual) {
338 | var message = arguments[2] || 'assertMatch';
339 | var regex = new RegExp(expected);
340 | try { (regex.exec(actual)) ? this.pass() :
341 | this.fail(message + ' : regex: "' + Test.Unit.inspect(expected) + ' did not match: ' + Test.Unit.inspect(actual) + '"'); }
342 | catch(e) { this.error(e); }
343 | },
344 | assertHidden: function(element) {
345 | var message = arguments[1] || 'assertHidden';
346 | this.assertEqual("none", element.style.display, message);
347 | },
348 | assertNotNull: function(object) {
349 | var message = arguments[1] || 'assertNotNull';
350 | this.assert(object != null, message);
351 | },
352 | assertType: function(expected, actual) {
353 | var message = arguments[2] || 'assertType';
354 | try {
355 | (actual.constructor == expected) ? this.pass() :
356 | this.fail(message + ': expected "' + Test.Unit.inspect(expected) +
357 | '", actual "' + (actual.constructor) + '"'); }
358 | catch(e) { this.error(e); }
359 | },
360 | assertNotOfType: function(expected, actual) {
361 | var message = arguments[2] || 'assertNotOfType';
362 | try {
363 | (actual.constructor != expected) ? this.pass() :
364 | this.fail(message + ': expected "' + Test.Unit.inspect(expected) +
365 | '", actual "' + (actual.constructor) + '"'); }
366 | catch(e) { this.error(e); }
367 | },
368 | assertInstanceOf: function(expected, actual) {
369 | var message = arguments[2] || 'assertInstanceOf';
370 | try {
371 | (actual instanceof expected) ? this.pass() :
372 | this.fail(message + ": object was not an instance of the expected type"); }
373 | catch(e) { this.error(e); }
374 | },
375 | assertNotInstanceOf: function(expected, actual) {
376 | var message = arguments[2] || 'assertNotInstanceOf';
377 | try {
378 | !(actual instanceof expected) ? this.pass() :
379 | this.fail(message + ": object was an instance of the not expected type"); }
380 | catch(e) { this.error(e); }
381 | },
382 | assertRespondsTo: function(method, obj) {
383 | var message = arguments[2] || 'assertRespondsTo';
384 | try {
385 | (obj[method] && typeof obj[method] == 'function') ? this.pass() :
386 | this.fail(message + ": object doesn't respond to [" + method + "]"); }
387 | catch(e) { this.error(e); }
388 | },
389 | assertReturnsTrue: function(method, obj) {
390 | var message = arguments[2] || 'assertReturnsTrue';
391 | try {
392 | var m = obj[method];
393 | if(!m) m = obj['is'+method.charAt(0).toUpperCase()+method.slice(1)];
394 | m() ? this.pass() :
395 | this.fail(message + ": method returned false"); }
396 | catch(e) { this.error(e); }
397 | },
398 | assertReturnsFalse: function(method, obj) {
399 | var message = arguments[2] || 'assertReturnsFalse';
400 | try {
401 | var m = obj[method];
402 | if(!m) m = obj['is'+method.charAt(0).toUpperCase()+method.slice(1)];
403 | !m() ? this.pass() :
404 | this.fail(message + ": method returned true"); }
405 | catch(e) { this.error(e); }
406 | },
407 | assertRaise: function(exceptionName, method) {
408 | var message = arguments[2] || 'assertRaise';
409 | try {
410 | method();
411 | this.fail(message + ": exception expected but none was raised"); }
412 | catch(e) {
413 | ((exceptionName == null) || (e.name==exceptionName)) ? this.pass() : this.error(e);
414 | }
415 | },
416 | assertElementsMatch: function() {
417 | var expressions = $A(arguments), elements = $A(expressions.shift());
418 | if (elements.length != expressions.length) {
419 | this.fail('assertElementsMatch: size mismatch: ' + elements.length + ' elements, ' + expressions.length + ' expressions');
420 | return false;
421 | }
422 | elements.zip(expressions).all(function(pair, index) {
423 | var element = $(pair.first()), expression = pair.last();
424 | if (element.match(expression)) return true;
425 | this.fail('assertElementsMatch: (in index ' + index + ') expected ' + expression.inspect() + ' but got ' + element.inspect());
426 | }.bind(this)) && this.pass();
427 | },
428 | assertElementMatches: function(element, expression) {
429 | this.assertElementsMatch([element], expression);
430 | },
431 | benchmark: function(operation, iterations) {
432 | var startAt = new Date();
433 | (iterations || 1).times(operation);
434 | var timeTaken = ((new Date())-startAt);
435 | this.info((arguments[2] || 'Operation') + ' finished ' +
436 | iterations + ' iterations in ' + (timeTaken/1000)+'s' );
437 | return timeTaken;
438 | },
439 | _isVisible: function(element) {
440 | element = $(element);
441 | if(!element.parentNode) return true;
442 | this.assertNotNull(element);
443 | if(element.style && Element.getStyle(element, 'display') == 'none')
444 | return false;
445 |
446 | return this._isVisible(element.parentNode);
447 | },
448 | assertNotVisible: function(element) {
449 | this.assert(!this._isVisible(element), Test.Unit.inspect(element) + " was not hidden and didn't have a hidden parent either. " + ("" || arguments[1]));
450 | },
451 | assertVisible: function(element) {
452 | this.assert(this._isVisible(element), Test.Unit.inspect(element) + " was not visible. " + ("" || arguments[1]));
453 | },
454 | benchmark: function(operation, iterations) {
455 | var startAt = new Date();
456 | (iterations || 1).times(operation);
457 | var timeTaken = ((new Date())-startAt);
458 | this.info((arguments[2] || 'Operation') + ' finished ' +
459 | iterations + ' iterations in ' + (timeTaken/1000)+'s' );
460 | return timeTaken;
461 | }
462 | };
463 |
464 | Test.Unit.Testcase = Class.create();
465 | Object.extend(Object.extend(Test.Unit.Testcase.prototype, Test.Unit.Assertions.prototype), {
466 | initialize: function(name, test, setup, teardown) {
467 | Test.Unit.Assertions.prototype.initialize.bind(this)();
468 | this.name = name;
469 |
470 | if(typeof test == 'string') {
471 | test = test.gsub(/(\.should[^\(]+\()/,'#{0}this,');
472 | test = test.gsub(/(\.should[^\(]+)\(this,\)/,'#{1}(this)');
473 | this.test = function() {
474 | eval('with(this){'+test+'}');
475 | }
476 | } else {
477 | this.test = test || function() {};
478 | }
479 |
480 | this.setup = setup || function() {};
481 | this.teardown = teardown || function() {};
482 | this.isWaiting = false;
483 | this.timeToWait = 1000;
484 | },
485 | wait: function(time, nextPart) {
486 | this.isWaiting = true;
487 | this.test = nextPart;
488 | this.timeToWait = time;
489 | },
490 | run: function() {
491 | try {
492 | try {
493 | if (!this.isWaiting) this.setup.bind(this)();
494 | this.isWaiting = false;
495 | this.test.bind(this)();
496 | } finally {
497 | if(!this.isWaiting) {
498 | this.teardown.bind(this)();
499 | }
500 | }
501 | }
502 | catch(e) { this.error(e); }
503 | }
504 | });
505 |
506 | // *EXPERIMENTAL* BDD-style testing to please non-technical folk
507 | // This draws many ideas from RSpec http://rspec.rubyforge.org/
508 |
509 | Test.setupBDDExtensionMethods = function(){
510 | var METHODMAP = {
511 | shouldEqual: 'assertEqual',
512 | shouldNotEqual: 'assertNotEqual',
513 | shouldEqualEnum: 'assertEnumEqual',
514 | shouldBeA: 'assertType',
515 | shouldNotBeA: 'assertNotOfType',
516 | shouldBeAn: 'assertType',
517 | shouldNotBeAn: 'assertNotOfType',
518 | shouldBeNull: 'assertNull',
519 | shouldNotBeNull: 'assertNotNull',
520 |
521 | shouldBe: 'assertReturnsTrue',
522 | shouldNotBe: 'assertReturnsFalse',
523 | shouldRespondTo: 'assertRespondsTo'
524 | };
525 | var makeAssertion = function(assertion, args, object) {
526 | this[assertion].apply(this,(args || []).concat([object]));
527 | };
528 |
529 | Test.BDDMethods = {};
530 | $H(METHODMAP).each(function(pair) {
531 | Test.BDDMethods[pair.key] = function() {
532 | var args = $A(arguments);
533 | var scope = args.shift();
534 | makeAssertion.apply(scope, [pair.value, args, this]); };
535 | });
536 |
537 | [Array.prototype, String.prototype, Number.prototype, Boolean.prototype].each(
538 | function(p){ Object.extend(p, Test.BDDMethods) }
539 | );
540 | };
541 |
542 | Test.context = function(name, spec, log){
543 | Test.setupBDDExtensionMethods();
544 |
545 | var compiledSpec = {};
546 | var titles = {};
547 | for(specName in spec) {
548 | switch(specName){
549 | case "setup":
550 | case "teardown":
551 | compiledSpec[specName] = spec[specName];
552 | break;
553 | default:
554 | var testName = 'test'+specName.gsub(/\s+/,'-').camelize();
555 | var body = spec[specName].toString().split('\n').slice(1);
556 | if(/^\{/.test(body[0])) body = body.slice(1);
557 | body.pop();
558 | body = body.map(function(statement){
559 | return statement.strip()
560 | });
561 | compiledSpec[testName] = body.join('\n');
562 | titles[testName] = specName;
563 | }
564 | }
565 | new Test.Unit.Runner(compiledSpec, { titles: titles, testLog: log || 'testlog', context: name });
566 | };
--------------------------------------------------------------------------------
/timeframe.js:
--------------------------------------------------------------------------------
1 | /* Timeframe, version 0.3.1
2 | * (c) 2008-2011 Stephen Celis
3 | *
4 | * Freely distributable under the terms of an MIT-style license.
5 | * ------------------------------------------------------------- */
6 |
7 | if (typeof Prototype == 'undefined' || parseFloat(Prototype.Version.substring(0, 3)) < 1.6)
8 | throw 'Timeframe requires Prototype version 1.6 or greater.';
9 |
10 | // Checks for localized Datejs before defaulting to 'en-US'
11 | var Locale = $H({
12 | format: (typeof Date.CultureInfo == 'undefined' ? '%b %d, %Y' : Date.CultureInfo.formatPatterns.shortDate),
13 | monthNames: (typeof Date.CultureInfo == 'undefined' ? $w('January February March April May June July August September October November December') : Date.CultureInfo.monthNames),
14 | dayNames: (typeof Date.CultureInfo == 'undefined' ? $w('Sunday Monday Tuesday Wednesday Thursday Friday Saturday') : Date.CultureInfo.dayNames),
15 | weekOffset: (typeof Date.CultureInfo == 'undefined' ? 0 : Date.CultureInfo.firstDayOfWeek)
16 | });
17 |
18 | var Timeframes = [];
19 |
20 | var Timeframe = Class.create({
21 | Version: '0.3',
22 |
23 | initialize: function(element, options) {
24 | Timeframes.push(this);
25 |
26 | this.element = $(element);
27 | this.element.addClassName('timeframe_calendar');
28 | this.options = $H({ months: 2 }).merge(options || {});;
29 | this.months = this.options.get('months');
30 |
31 | this.weekdayNames = Locale.get('dayNames');
32 | this.monthNames = Locale.get('monthNames');
33 | this.format = this.options.get('format') || Locale.get('format');
34 | this.weekOffset = this.options.get('weekOffset') || Locale.get('weekOffset');
35 | this.maxRange = this.options.get('maxRange');
36 |
37 | this.firstDayId = this.element.id + '_firstday';
38 | this.lastDayId = this.element.id + '_lastday';
39 |
40 | this.scrollerDelay = 0.5;
41 |
42 | this.buttons = $H({
43 | previous: $H({ label: '←', element: $(this.options.get('previousButton')) }),
44 | today: $H({ label: 'T', element: $(this.options.get('todayButton')) }),
45 | reset: $H({ label: 'R', element: $(this.options.get('resetButton')) }),
46 | next: $H({ label: '→', element: $(this.options.get('nextButton')) })
47 | });
48 | this.fields = $H({ start: $(this.options.get('startField')), end: $(this.options.get('endField')) });
49 |
50 | this.range = $H({});
51 | this.earliest = Date.parseToObject(this.options.get('earliest'));
52 | this.latest = Date.parseToObject(this.options.get('latest'));
53 | if (this.earliest && this.latest && this.earliest > this.latest)
54 | throw new Error("Timeframe: 'earliest' cannot come later than 'latest'");
55 |
56 | this._buildButtons()._buildFields();
57 |
58 | this.calendars = [];
59 | this.element.insert(new Element('div', { id: this.element.id + '_container' }));
60 | this.months.times(function(month) { this.createCalendar(month) }.bind(this));
61 |
62 | this.calendars.first().select('td').first().id = this.firstDayId;
63 | this.calendars.last().select('td').last().id = this.lastDayId;
64 |
65 | this.register().populate().refreshRange();
66 | },
67 |
68 | // Scaffolding
69 |
70 | createCalendar: function() {
71 | var calendar = new Element('table', {
72 | id: this.element.id + '_calendar_' + this.calendars.length, border: 0, cellspacing: 0, cellpadding: 5
73 | });
74 | calendar.insert(new Element('caption'));
75 |
76 | var head = new Element('thead');
77 | var row = new Element('tr');
78 | this.weekdayNames.length.times(function(column) {
79 | var weekday = this.weekdayNames[(column + this.weekOffset) % 7];
80 | var cell = new Element('th', { scope: 'col', abbr: weekday }).update(weekday.substring(0,1));
81 | row.insert(cell);
82 | }.bind(this));
83 | head.insert(row);
84 | calendar.insert(head);
85 |
86 | var body = new Element('tbody');
87 | (6).times(function(rowNumber) {
88 | var row = new Element('tr');
89 | this.weekdayNames.length.times(function(column) {
90 | var cell = new Element('td');
91 | row.insert(cell);
92 | });
93 | body.insert(row);
94 | }.bind(this));
95 | calendar.insert(body);
96 |
97 | this.element.down('div#' + this.element.id + '_container').insert(calendar);
98 | this.calendars.push(calendar);
99 | this.months = this.calendars.length;
100 |
101 | return this;
102 | },
103 |
104 | destroyCalendar: function() {
105 | this.calendars.pop().remove();
106 | this.months = this.calendars.length;
107 | return this;
108 | },
109 |
110 | populate: function() {
111 | var month = this.date.neutral();
112 | month.setDate(1);
113 |
114 | if (this.earliest === null || this.earliest < month)
115 | this.buttons.get('previous').get('element').removeClassName('disabled');
116 | else
117 | this.buttons.get('previous').get('element').addClassName('disabled');
118 |
119 | this.calendars.each(function(calendar) {
120 | var caption = calendar.select('caption').first();
121 | caption.update(this.monthNames[month.getMonth()] + ' ' + month.getFullYear());
122 |
123 | var iterator = new Date(month);
124 | var offset = (iterator.getDay() - this.weekOffset) % 7;
125 | var inactive = offset > 0 ? 'pre beyond' : false;
126 | iterator.setDate(iterator.getDate() - offset);
127 | if (iterator.getDate() > 1 && !inactive) {
128 | iterator.setDate(iterator.getDate() - 7);
129 | if (iterator.getDate() > 1) inactive = 'pre beyond';
130 | }
131 |
132 | calendar.select('td').each(function(day) {
133 | day.date = new Date(iterator); // Is this expensive (we unload these later)? We could store the epoch time instead.
134 | day.update(day.date.getDate()).writeAttribute('class', inactive || 'active');
135 | if ((this.earliest && day.date < this.earliest) || (this.latest && day.date > this.latest))
136 | day.addClassName('unselectable');
137 | else
138 | day.addClassName('selectable');
139 | if (iterator.toString() === new Date().neutral().toString()) day.addClassName('today');
140 | day.baseClass = day.readAttribute('class');
141 |
142 | iterator.setDate(iterator.getDate() + 1);
143 | if (iterator.getDate() == 1) inactive = inactive ? false : 'post beyond';
144 | }.bind(this));
145 |
146 | month.setMonth(month.getMonth() + 1);
147 | }.bind(this));
148 |
149 | if (this.latest === null || this.latest > month)
150 | this.buttons.get('next').get('element').removeClassName('disabled');
151 | else
152 | this.buttons.get('next').get('element').addClassName('disabled');
153 |
154 | return this;
155 | },
156 |
157 | _buildButtons: function() {
158 | var buttonList = new Element('ul', { id: this.element.id + '_menu', className: 'timeframe_menu' });
159 | this.buttons.each(function(pair) {
160 | if (pair.value.get('element'))
161 | pair.value.get('element').addClassName('timeframe_button').addClassName(pair.key);
162 | else {
163 | var item = new Element('li');
164 | var button = new Element('a', { className: 'timeframe_button ' + pair.key, href: '#', onclick: 'return false;' }).update(pair.value.get('label'));
165 | button.onclick = function() { return false; };
166 | pair.value.set('element', button);
167 | item.insert(button);
168 | buttonList.insert(item);
169 | }
170 | }.bind(this));
171 | if (buttonList.childNodes.length > 0) this.element.insert({ top: buttonList });
172 | this.clearButton = new Element('span', { className: 'clear' }).update(new Element('span').update('X'));
173 | return this;
174 | },
175 |
176 | _buildFields: function() {
177 | var fieldset = new Element('div', { id: this.element.id + '_fields', className: 'timeframe_fields' });
178 | this.fields.each(function(pair) {
179 | if (pair.value)
180 | pair.value.addClassName('timeframe_field').addClassName(pair.key);
181 | else {
182 | var container = new Element('div', { id: pair.key + this.element.id + '_field_container' });
183 | this.fields.set(pair.key, new Element('input', { id: this.element.id + '_' + pair.key + 'field', name: pair.key + 'field', type: 'text', value: '' }));
184 | container.insert(new Element('label', { 'for': pair.key + 'field' }).update(pair.key));
185 | container.insert(this.fields.get(pair.key));
186 | fieldset.insert(container);
187 | }
188 | }.bind(this));
189 | if (fieldset.childNodes.length > 0) this.element.insert(fieldset);
190 | this.parseField('start').refreshField('start').parseField('end').refreshField('end').initDate = new Date(this.date);
191 | return this;
192 | },
193 |
194 | // Event registration
195 |
196 | register: function() {
197 | document.observe('click', this.eventClick.bind(this));
198 | this.element.observe('mousedown', this.eventMouseDown.bind(this));
199 | this.element.observe('mouseover', this.eventMouseOver.bind(this));
200 | $(this.firstDayId).observe('mouseout', this.clearTimer.bind(this));
201 | $(this.lastDayId).observe('mouseout', this.clearTimer.bind(this));
202 | document.observe('mouseup', this.eventMouseUp.bind(this));
203 | document.observe('unload', this.unregister.bind(this));
204 | // mousemove listener for Opera in _disableTextSelection
205 | return this._registerFieldObserver('start')._registerFieldObserver('end')._disableTextSelection();
206 | },
207 |
208 | unregister: function() {
209 | this.element.select('td').each(function(day) { day.date = day.baseClass = null; });
210 | },
211 |
212 | _registerFieldObserver: function(fieldName) {
213 | var field = this.fields.get(fieldName);
214 | field.observe('focus', function() { field.hasFocus = true; this.parseField(fieldName, true); }.bind(this));
215 | field.observe('blur', function() { this.refreshField(fieldName); }.bind(this));
216 | new Form.Element.Observer(field, 0.2, function(element, value) { if (element.hasFocus) this.parseField(fieldName, true); }.bind(this));
217 | return this;
218 | },
219 |
220 | _disableTextSelection: function() {
221 | if (Prototype.Browser.IE) {
222 | this.element.onselectstart = function(event) {
223 | if (!/input|textarea/i.test(Event.element(event).tagName)) return false;
224 | };
225 | } else if (Prototype.Browser.Opera)
226 | document.observe('mousemove', this.handleMouseMove.bind(this));
227 | else {
228 | this.element.onmousedown = function(event) {
229 | if (!/input|textarea/i.test(Event.element(event).tagName)) return false;
230 | };
231 | }
232 | return this;
233 | },
234 |
235 | // Fields
236 |
237 | parseField: function(fieldName, populate) {
238 | var field = this.fields.get(fieldName);
239 | var date = Date.parseToObject($F(this.fields.get(fieldName)));
240 | var failure = this.validateField(fieldName, date);
241 | if (failure != 'hard') {
242 | this.range.set(fieldName, date);
243 | field.removeClassName('error');
244 | } else if (field.hasFocus)
245 | field.addClassName('error');
246 | var date = Date.parseToObject(this.range.get(fieldName));
247 | this.date = date || new Date();
248 | if (this.earliest && this.earliest > this.date) {
249 | this.date = new Date(this.earliest);
250 | } else if (this.latest) {
251 | date = new Date(this.date);
252 | date.setMonth(date.getMonth() + (this.months - 1));
253 | if (date > this.latest) {
254 | this.date = new Date(this.latest);
255 | this.date.setMonth(this.date.getMonth() - (this.months - 1));
256 | }
257 | }
258 | this.date.setDate(1);
259 | if (populate && date) this.populate();
260 | this.refreshRange();
261 | return this;
262 | },
263 |
264 | refreshField: function(fieldName) {
265 | var field = this.fields.get(fieldName);
266 | var initValue = $F(field);
267 | if (this.range.get(fieldName)) {
268 | field.setValue(typeof Date.CultureInfo == 'undefined' ?
269 | this.range.get(fieldName).strftime(this.format) :
270 | this.range.get(fieldName).toString(this.format));
271 | } else
272 | field.setValue('');
273 | field.hasFocus && $F(field) == '' && initValue != '' ? field.addClassName('error') : field.removeClassName('error');
274 | field.hasFocus = false;
275 | return this;
276 | },
277 |
278 | validateField: function(fieldName, date) {
279 | if (!date) return;
280 | var error;
281 | if ((this.earliest && date < this.earliest) || (this.latest && date > this.latest))
282 | error = 'hard';
283 | else if (fieldName == 'start' && this.range.get('end') && date > this.range.get('end'))
284 | error = 'soft';
285 | else if (fieldName == 'end' && this.range.get('start') && date < this.range.get('start'))
286 | error = 'soft';
287 | return error;
288 | },
289 |
290 | // Event handling
291 |
292 | eventClick: function(event) {
293 | if (!event.element().ancestors) return;
294 | var el;
295 | if (el = event.findElement('a.timeframe_button'))
296 | this.handleButtonClick(event, el);
297 | },
298 |
299 | eventMouseDown: function(event) {
300 | if (!event.element().ancestors) return;
301 | var el, em;
302 | if (el = event.findElement('span.clear')) {
303 | el.down('span').addClassName('active');
304 | if (em = event.findElement('td.selectable'))
305 | this.handleDateClick(em, true);
306 | } else if (el = event.findElement('td.selectable'))
307 | this.handleDateClick(el);
308 | else return;
309 | },
310 |
311 | handleButtonClick: function(event, element) {
312 | var el;
313 | var movement = this.months > 1 ? this.months - 1 : 1;
314 | if (element.hasClassName('next')) {
315 | if (!this.buttons.get('next').get('element').hasClassName('disabled'))
316 | this.date.setMonth(this.date.getMonth() + movement);
317 | } else if (element.hasClassName('previous')) {
318 | if (!this.buttons.get('previous').get('element').hasClassName('disabled'))
319 | this.date.setMonth(this.date.getMonth() - movement);
320 | } else if (element.hasClassName('today'))
321 | this.date = new Date();
322 | else if (element.hasClassName('reset'))
323 | this.reset();
324 | this.populate().refreshRange();
325 | },
326 |
327 | reset: function() {
328 | this.fields.get('start').setValue(this.fields.get('start').defaultValue || '');
329 | this.fields.get('end').setValue(this.fields.get('end').defaultValue || '');
330 | this.date = new Date(this.initDate);
331 | this.parseField('start').refreshField('start').parseField('end').refreshField('end');
332 | },
333 |
334 | clear: function() {
335 | this.clearRange();
336 | this.refreshRange();
337 | },
338 |
339 | handleDateClick: function(element, couldClear) {
340 | this.mousedown = this.dragging = true;
341 | if (this.stuck) {
342 | this.stuck = false;
343 | return;
344 | } else if (couldClear) {
345 | if (!element.hasClassName('startrange')) return;
346 | } else if (this.maxRange != 1) {
347 | this.stuck = true;
348 | setTimeout(function() { if (this.mousedown) this.stuck = false; }.bind(this), 200);
349 | }
350 | this.getPoint(element.date);
351 | },
352 |
353 | getPoint: function(date) {
354 | if (this.range.get('start') && this.range.get('start').toString() == date && this.range.get('end'))
355 | this.startdrag = this.range.get('end');
356 | else {
357 | this.clearButton.hide();
358 | if (this.range.get('end') && this.range.get('end').toString() == date)
359 | this.startdrag = this.range.get('start');
360 | else
361 | this.startdrag = this.range.set('start', this.range.set('end', date));
362 | }
363 | this.refreshRange();
364 | },
365 |
366 | eventMouseOver: function(event) {
367 | var el;
368 | if (!this.dragging)
369 | this.toggleClearButton(event);
370 | else if (event.findElement('span.clear span.active'));
371 | else if (el = event.findElement('td.selectable')) {
372 | window.clearInterval(this.timer);
373 | if (el.id == this.lastDayId) {
374 | this.timer = window.setInterval(function() {
375 | if (!this.buttons.get('next').get('element').hasClassName('disabled')) {
376 | this.date.setMonth(this.date.getMonth() + 1);
377 | this.populate().refreshRange();
378 | }
379 | }.bind(this), this.scrollerDelay * 1000);
380 | } else if (el.id == this.firstDayId) {
381 | this.timer = window.setInterval(function() {
382 | if (!this.buttons.get('previous').get('element').hasClassName('disabled')) {
383 | this.date.setMonth(this.date.getMonth() - 1);
384 | this.populate().refreshRange();
385 | }
386 | }.bind(this), this.scrollerDelay * 1000);
387 | }
388 | this.extendRange(el.date);
389 | } else this.toggleClearButton(event);
390 | },
391 |
392 | clearTimer: function(event) {
393 | window.clearInterval(this.timer);
394 | return this;
395 | },
396 |
397 | toggleClearButton: function(event) {
398 | var el;
399 | if (event.element().ancestors && event.findElement('td.selected')) {
400 | if (el = this.element.select('#' + this.calendars.first().id + ' .pre.selected').first());
401 | else if (el = this.element.select('.active.selected').first());
402 | else if (el = this.element.select('.post.selected').first());
403 | if (el) Element.insert(el, { top: this.clearButton });
404 | this.clearButton.show().select('span').first().removeClassName('active');
405 | } else
406 | this.clearButton.hide();
407 | },
408 |
409 | extendRange: function(date) {
410 | var start, end;
411 | this.clearButton.hide();
412 | if (date > this.startdrag) {
413 | start = this.startdrag;
414 | end = date;
415 | } else if (date < this.startdrag) {
416 | start = date;
417 | end = this.startdrag;
418 | } else
419 | start = end = date;
420 | this.validateRange(start, end);
421 | this.refreshRange();
422 | },
423 |
424 | validateRange: function(start, end) {
425 | if (this.maxRange) {
426 | var range = this.maxRange - 1;
427 | var days = parseInt((end - start) / 86400000);
428 | if (days > range) {
429 | if (start == this.startdrag) {
430 | end = new Date(this.startdrag);
431 | end.setDate(end.getDate() + range);
432 | } else {
433 | start = new Date(this.startdrag);
434 | start.setDate(start.getDate() - range);
435 | }
436 | }
437 | }
438 | this.range.set('start', start);
439 | this.range.set('end', end);
440 | },
441 |
442 | eventMouseUp: function(event) {
443 | if (!this.dragging) return;
444 | if (!this.stuck) {
445 | this.dragging = false;
446 | if (this.timer) {
447 | clearInterval(this.timer);
448 | }
449 | if (event.findElement('span.clear span.active')) {
450 | this.clearRange();
451 | } else if (this.options.keys().include('onFinished')) {
452 | this.options.get('onFinished')();
453 | }
454 | }
455 | this.mousedown = false;
456 | this.refreshRange();
457 | },
458 |
459 | clearRange: function() {
460 | this.clearButton.hide().select('span').first().removeClassName('active');
461 | this.range.set('start', this.range.set('end', null));
462 | this.refreshField('start').refreshField('end');
463 | if (this.options.keys().include('onClear')) this.options.get('onClear')();
464 | },
465 |
466 | refreshRange: function() {
467 | this.element.select('td').each(function(day) {
468 | day.writeAttribute('class', day.baseClass);
469 | if (this.range.get('start') && this.range.get('end') && this.range.get('start') <= day.date && day.date <= this.range.get('end')) {
470 | var baseClass = day.hasClassName('beyond') ? 'beyond_' : day.hasClassName('today') ? 'today_' : null;
471 | var state = this.stuck || this.mousedown ? 'stuck' : 'selected';
472 | if (baseClass) day.addClassName(baseClass + state);
473 | day.addClassName(state);
474 | var rangeClass = '';
475 | if (this.range.get('start').toString() == day.date) rangeClass += 'start';
476 | if (this.range.get('end').toString() == day.date) rangeClass += 'end';
477 | if (rangeClass.length > 0) day.addClassName(rangeClass + 'range');
478 | }
479 | if (Prototype.Browser.Opera) {
480 | day.unselectable = 'on'; // Trick Opera into refreshing the selection (FIXME)
481 | day.unselectable = null;
482 | }
483 | }.bind(this));
484 | if (this.dragging) this.refreshField('start').refreshField('end');
485 | },
486 |
487 | setRange: function(start, end) {
488 | var range = $H({ start: start, end: end });
489 | range.each(function(pair) {
490 | this.range.set(pair.key, Date.parseToObject(pair.value));
491 | this.refreshField(pair.key);
492 | this.parseField(pair.key, true);
493 | }.bind(this));
494 | return this;
495 | },
496 |
497 | handleMouseMove: function(event) {
498 | if (event.findElement('#' + this.element.id + ' td')) window.getSelection().removeAllRanges(); // More Opera trickery
499 | }
500 | });
501 |
502 | Object.extend(Date, {
503 | parseToObject: function(string) {
504 | var date = Date.parse(string);
505 | if (!date) return null;
506 | date = new Date(date);
507 | return (date == 'Invalid Date' || date == 'NaN') ? null : date.neutral();
508 | }
509 | });
510 |
511 | Object.extend(Date.prototype, {
512 | // modified from http://alternateidea.com/blog/articles/2008/2/8/a-strftime-for-prototype
513 | strftime: function(format) {
514 | var day = this.getDay(), month = this.getMonth();
515 | var hours = this.getHours(), minutes = this.getMinutes();
516 | function pad(num) { return num.toPaddedString(2); };
517 |
518 | return format.gsub(/\%([aAbBcdHImMpsSwyY])/, function(part) {
519 | switch(part[1]) {
520 | case 'a': return Locale.get('dayNames').invoke('substring', 0, 3)[day].escapeHTML(); break;
521 | case 'A': return Locale.get('dayNames')[day].escapeHTML(); break;
522 | case 'b': return Locale.get('monthNames').invoke('substring', 0, 3)[month].escapeHTML(); break;
523 | case 'B': return Locale.get('monthNames')[month].escapeHTML(); break;
524 | case 'c': return this.toString(); break;
525 | case 'd': return pad(this.getDate()); break;
526 | case 'H': return pad(hours); break;
527 | case 'I': return (hours % 12 == 0) ? 12 : pad(hours % 12); break;
528 | case 'm': return pad(month + 1); break;
529 | case 'M': return pad(minutes); break;
530 | case 'p': return hours >= 12 ? 'PM' : 'AM'; break;
531 | case 's': return this.getTime()/1000;
532 | case 'S': return pad(this.getSeconds()); break;
533 | case 'w': return day; break;
534 | case 'y': return pad(this.getFullYear() % 100); break;
535 | case 'Y': return this.getFullYear().toString(); break;
536 | }
537 | }.bind(this));
538 | },
539 |
540 | neutral: function() {
541 | return new Date(this.getFullYear(), this.getMonth(), this.getDate(), 12);
542 | }
543 | });
544 |
--------------------------------------------------------------------------------