├── .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 |

Timeframe, by Stephen Celis

24 |

25 | Click-draggable. Range-makeable. A better calendar. 26 |

27 |

Click here for the example.

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 | 63 |

64 | An example: 65 |

66 | 70 |
71 |

Please select a date range below:

72 |
73 |
74 |
75 | and (reset): 76 |
77 |
78 | 79 | 80 | – 81 | 82 | 83 |
84 |
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 | Click here to lend your support to: timeframe and make a donation at www.pledgie.com ! 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 | '' + 116 | '' + 117 | '
StatusTestMessage
'; 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 | --------------------------------------------------------------------------------