mongoid_wice_grid 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. data/.gitignore +9 -0
  2. data/CHANGELOG +409 -0
  3. data/Gemfile +17 -0
  4. data/Gemfile.lock +140 -0
  5. data/MIT-LICENSE +20 -0
  6. data/README.rdoc +1188 -0
  7. data/Rakefile +40 -0
  8. data/SAVED_QUERIES_HOWTO.rdoc +123 -0
  9. data/VERSION +1 -0
  10. data/lib/filter_conditions_generators.rb +126 -0
  11. data/lib/generators/wice_grid/templates/calendarview.css +107 -0
  12. data/lib/generators/wice_grid/templates/calendarview.js +1168 -0
  13. data/lib/generators/wice_grid/templates/icons/arrow_down.gif +0 -0
  14. data/lib/generators/wice_grid/templates/icons/arrow_up.gif +0 -0
  15. data/lib/generators/wice_grid/templates/icons/calendar_view_month.png +0 -0
  16. data/lib/generators/wice_grid/templates/icons/delete.png +0 -0
  17. data/lib/generators/wice_grid/templates/icons/expand.png +0 -0
  18. data/lib/generators/wice_grid/templates/icons/page_white_excel.png +0 -0
  19. data/lib/generators/wice_grid/templates/icons/page_white_find.png +0 -0
  20. data/lib/generators/wice_grid/templates/icons/table.png +0 -0
  21. data/lib/generators/wice_grid/templates/icons/table_refresh.png +0 -0
  22. data/lib/generators/wice_grid/templates/icons/tick_all.png +0 -0
  23. data/lib/generators/wice_grid/templates/icons/untick_all.png +0 -0
  24. data/lib/generators/wice_grid/templates/wice_grid.css +173 -0
  25. data/lib/generators/wice_grid/templates/wice_grid.yml +269 -0
  26. data/lib/generators/wice_grid/templates/wice_grid_config.rb +215 -0
  27. data/lib/generators/wice_grid/templates/wice_grid_jquery.js +161 -0
  28. data/lib/generators/wice_grid/templates/wice_grid_prototype.js +153 -0
  29. data/lib/generators/wice_grid/wice_grid_assets_jquery_generator.rb +32 -0
  30. data/lib/generators/wice_grid/wice_grid_assets_prototype_generator.rb +34 -0
  31. data/lib/grid_output_buffer.rb +52 -0
  32. data/lib/grid_renderer.rb +547 -0
  33. data/lib/helpers/js_calendar_helpers.rb +183 -0
  34. data/lib/helpers/wice_grid_misc_view_helpers.rb +113 -0
  35. data/lib/helpers/wice_grid_serialized_queries_view_helpers.rb +82 -0
  36. data/lib/helpers/wice_grid_view_helpers.rb +761 -0
  37. data/lib/js_adaptors/jquery_adaptor.rb +145 -0
  38. data/lib/js_adaptors/js_adaptor.rb +12 -0
  39. data/lib/js_adaptors/prototype_adaptor.rb +168 -0
  40. data/lib/mongoid_field.rb +50 -0
  41. data/lib/tasks/wice_grid_tasks.rake +28 -0
  42. data/lib/view_columns.rb +464 -0
  43. data/lib/views/create.rjs +13 -0
  44. data/lib/views/delete.rjs +12 -0
  45. data/lib/wice_grid.rb +521 -0
  46. data/lib/wice_grid_controller.rb +165 -0
  47. data/lib/wice_grid_core_ext.rb +179 -0
  48. data/lib/wice_grid_misc.rb +99 -0
  49. data/lib/wice_grid_serialized_queries_controller.rb +77 -0
  50. data/lib/wice_grid_serialized_query.rb +14 -0
  51. data/lib/wice_grid_spreadsheet.rb +33 -0
  52. data/test/.gitignore +2 -0
  53. data/test/blueprint.rb +17 -0
  54. data/test/database.yml +21 -0
  55. data/test/public/javascripts/jquery-1.4.2.min.js +154 -0
  56. data/test/public/javascripts/wice_grid.js +163 -0
  57. data/test/rails_mongoid_test.rb +104 -0
  58. data/test/rails_test_app.rb +71 -0
  59. data/test/require_gems.rb +19 -0
  60. data/test/schema.rb +33 -0
  61. data/test/spec_helper.rb +22 -0
  62. data/test/test_helper.rb +89 -0
  63. data/test/views/projects_and_people_grid.html.erb +12 -0
  64. data/test/views/projects_and_people_grid_invalid.html.erb +12 -0
  65. data/test/views/simple_projects_grid.html.erb +9 -0
  66. data/test/wice_grid_core_ext_test.rb +183 -0
  67. data/test/wice_grid_functional_test.rb +68 -0
  68. data/test/wice_grid_initializer.rb +215 -0
  69. data/test/wice_grid_misc_test.rb +41 -0
  70. data/test/wice_grid_test.rb +42 -0
  71. data/test/wice_grid_view_helper_test.rb +12 -0
  72. metadata +150 -0
data/Rakefile ADDED
@@ -0,0 +1,40 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test the wice_grid plugin.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.pattern = 'test/**/*_test.rb'
12
+ t.verbose = true
13
+ end
14
+
15
+ desc 'Generate documentation for the wice_grid plugin.'
16
+ Rake::RDocTask.new(:rdoc) do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = 'WiceGrid'
19
+ rdoc.options << '--line-numbers' << '--inline-source'
20
+ rdoc.rdoc_files.include('README.rdoc')
21
+ rdoc.rdoc_files.include('SAVED_QUERIES_HOWTO.rdoc')
22
+ rdoc.rdoc_files.include('CHANGELOG')
23
+ rdoc.rdoc_files.include('lib/**/*.rb')
24
+ end
25
+
26
+ begin
27
+ require 'jeweler'
28
+ Jeweler::Tasks.new do |gem|
29
+ gem.name = "mongoid_wice_grid"
30
+ gem.summary = %Q{Rails Grid Plugin}
31
+ gem.description = %Q{A Rails grid plugin to create grids with sorting, pagination, and (automatically generated) filters }
32
+ gem.email = "aleksandr.furmanov@gmail.com"
33
+ gem.homepage = "https://github.com/afurmanov/wice_grid"
34
+ gem.authors = ["Yuri Leikind", "Aleksandr Furmanov"]
35
+ end
36
+ Jeweler::GemcutterTasks.new
37
+ rescue LoadError
38
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
39
+ end
40
+
@@ -0,0 +1,123 @@
1
+ == Saving Queries How-To
2
+
3
+ WiceGrid allows to save the state of filters as a custom query and later restore it from the list of saved queries.
4
+
5
+ === Step 1: Create the database table to store queries
6
+
7
+ To get started really fast create the database table to store queries with the following command:
8
+
9
+ rake wice_grid:create_queries_table
10
+
11
+ This will create the table, but it is preferred to manually create and run a migration in <tt>db/migrate</tt>:
12
+
13
+ class CreateWiceGridSerializedQueriesTable < ActiveRecord::Migration
14
+ def self.up
15
+ create_table :wice_grid_serialized_queries do |t|
16
+ t.column :name, :string
17
+ t.column :grid_name, :string
18
+ t.column :query, :text
19
+
20
+ t.timestamps
21
+ end
22
+ add_index :wice_grid_serialized_queries, :grid_name
23
+ add_index :wice_grid_serialized_queries, [:grid_name, :id]
24
+ end
25
+
26
+ def self.down
27
+ drop_table :wice_grid_serialized_queries
28
+ end
29
+ end
30
+
31
+ === Step 2: Create the controller to handle AJAX queries.
32
+
33
+ Creation and deletion of queries is implemented as AJAX calls, and a controller is needed to handle these calls. Create an empty controller
34
+ with any name and add method +save_wice_grid_queries+ to it:
35
+
36
+ class QueriesController < ApplicationController
37
+ save_wice_grid_queries
38
+ end
39
+
40
+ This is it. The controller now has the required action methods.
41
+
42
+ === Step 3: Add routes
43
+
44
+ If the name of the query controller is QueriesController, add the following to <tt>routes.rb</tt>:
45
+
46
+ Wice::define_routes(map, 'queries')
47
+
48
+ === Step 4: Add the saved query panel to the view.
49
+
50
+ To show the list of saved queries and the form to create new queries in a view, add the following helper to the view:
51
+
52
+ <%= saved_queries_panel(@grid_object) %>
53
+
54
+ Voila!
55
+
56
+ Just like WiceGrid itself, the query panel contains no forms and is thus form-friendly.
57
+
58
+ *Important*: Saved queries of all grids in the application are stored in one table and differentiated by the name of the grid, thus, for all forms
59
+ with saved queries it is important to define different names! (use parameter <tt>:name</tt> in +initialize_grid+)
60
+
61
+ It is also possible to initialize a grid with an initial saved query providing the id of the query record or the ActiveRecord
62
+ itself to parameter <tt>saved_query</tt>:
63
+
64
+ @products_grid = initialize_grid(Product,
65
+ :name => 'prod_grid',
66
+ :saved_query => SavedQuery.find_by_id_and_grid_name(12, 'prod_grid') )
67
+
68
+
69
+ == Adding Application Specific Logic to Saving/Restoring Queries
70
+
71
+ WiceGrid allows to add application specific logic to saving and restoring queries by substituting the default ActiveRecord model provided by WiceGrid with a custom model.
72
+
73
+ Copy <tt>vendor/plugins/wice_grid/lib/wice_grid_serialized_query.rb</tt> to <tt>app/models/</tt>, rename the file and the class to your liking.
74
+
75
+ Next, modify the model to suit your needs. Right after copying and renaming the model to SavedQuery it looks like this:
76
+
77
+ class SavedQuery < ActiveRecord::Base #:nodoc:
78
+ serialize :query
79
+
80
+ validates_uniqueness_of :name, :scope => :grid_name, :on => :create, :message => ::Wice::Defaults::VALIDATES_UNIQUENESS_ERROR
81
+ validates_presence_of :name, :message => ::Wice::Defaults::VALIDATES_PRESENCE_ERROR
82
+
83
+ def self.list(name, controller)
84
+ conditions = {:grid_name => name}
85
+ self.find(:all, :conditions => conditions)
86
+ end
87
+ end
88
+
89
+ It is required that the model provides class method +list+ which accepts two parameters: the name of the WiceGrid instance and the controller
90
+ object, and returns a list of queries. The controller object is needed to provide the application context. For instance, if it is needed to
91
+ store queries for each user, we could add +user_id+ to the table and modify the code so it looks like the following:
92
+
93
+ class SavedQuery < ActiveRecord::Base
94
+ serialize :query
95
+
96
+ # the scope is changed to include user_id
97
+ validates_uniqueness_of :name, :scope => [:grid_name, :user_id], :on => :create, :message => 'A query with this name already exists'
98
+ validates_presence_of :name, :message => 'Please submit the name of the query'
99
+
100
+ belongs_to :identity # !
101
+
102
+ def self.list(name, controller)
103
+ conditions = {:grid_name => name}
104
+ if controller.current_user # !
105
+ conditions[:user_id] = controller.current_user.id # provided that method current_user is defined in ApplicationController and returns the currrent user.
106
+ end
107
+ self.find(:all, :conditions => conditions)
108
+ end
109
+ end
110
+
111
+ The following step is to make sure that a new query is saved with the correct +user_id+. To do so, change the helper
112
+ <tt>saved_queries_panel(@grid_object)</tt> to the following:
113
+
114
+
115
+ <%= saved_queries_panel(@identities_grid, :extra_parameters => {:user_id => @current_user.id}) %>
116
+
117
+ For every key in has :extra_parameters there must exist a field in the model - this hash will be used as a parameter to
118
+ <tt>attributes=</tt> method of the query object.
119
+
120
+ Finally, let WiceGrid know which model to use for saving queries by changing constant +QUERY_STORE_MODEL+
121
+ in <tt>lib/wice_grid_config.rb</tt> to the name of the custom model (as a string), in the above example this would look like the following:
122
+
123
+ QUERY_STORE_MODEL = 'SavedQuery'
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 4.0.0
@@ -0,0 +1,126 @@
1
+ module Wice
2
+ class FilterConditionsGenerator #:nodoc:
3
+
4
+ cattr_accessor :handled_type
5
+ @@handled_type = HashWithIndifferentAccess.new
6
+
7
+ def initialize(field, criteria) #:nodoc:
8
+ @field = field
9
+ @criteria = criteria
10
+ end
11
+
12
+ def generate_conditions(opts)
13
+ raise "must be implemented"
14
+ end
15
+ end
16
+
17
+ # class FilterConditionsGeneratorCustomFilter < FilterConditionsGenerator #:nodoc:
18
+
19
+ # def generate_conditions(opts) #:nodoc:
20
+ # if opts.empty?
21
+ # Wice.log "empty parameters for the grid custom filter"
22
+ # return false
23
+ # end
24
+ # opts = (opts.kind_of?(Array) && opts.size == 1) ? opts[0] : opts
25
+
26
+ # if opts.kind_of?(Array)
27
+ # opts_with_special_values, normal_opts = opts.partition{|v| ::Wice::GridTools.special_value(v)}
28
+
29
+ # conditions_ar = if normal_opts.size > 0
30
+ # [" #{@field.alias_or_table_name(table_alias)}.#{@field.name} IN ( " + (['?'] * normal_opts.size).join(', ') + ' )'] + normal_opts
31
+ # else
32
+ # []
33
+ # end
34
+
35
+ # if opts_with_special_values.size > 0
36
+ # special_conditions = opts_with_special_values.collect{|v| " #{@field.alias_or_table_name(table_alias)}.#{@field.name} is " + v}.join(' or ')
37
+ # if conditions_ar.size > 0
38
+ # conditions_ar[0] = " (#{conditions_ar[0]} or #{special_conditions} ) "
39
+ # else
40
+ # conditions_ar = " ( #{special_conditions} ) "
41
+ # end
42
+ # end
43
+ # conditions_ar
44
+ # else
45
+ # if ::Wice::GridTools.special_value(opts)
46
+ # " #{@field.alias_or_table_name(table_alias)}.#{@field.name} is " + opts
47
+ # else
48
+ # [" #{@field.alias_or_table_name(table_alias)}.#{@field.name} = ?", opts]
49
+ # end
50
+ # end
51
+ # end
52
+
53
+ # end
54
+
55
+ class FilterConditionsGeneratorBoolean < FilterConditionsGenerator #:nodoc:
56
+ @@handled_type[Boolean] = self
57
+
58
+ def generate_conditions(opts) #:nodoc:
59
+ unless (opts.kind_of?(Array) && opts.size == 1 && ['f', 't'].include?(opts[0]))
60
+ Wice.log "invalid parameters for the grid boolean filter - must be an one item array: #{opts.inspect}"
61
+ return false
62
+ end
63
+ @criteria.where(@field.name.to_s => opts[0] == 't' ? true : false)
64
+ return true
65
+ end
66
+ end
67
+
68
+ class FilterConditionsGeneratorString < FilterConditionsGenerator #:nodoc:
69
+ @@handled_type[String] = self
70
+
71
+ def generate_conditions(opts) #:nodoc:
72
+ negation = nil
73
+ if opts.kind_of? String
74
+ string_fragment = opts
75
+ elsif (opts.kind_of? Hash) && opts.has_key?(:v)
76
+ string_fragment = opts[:v]
77
+ #negation = opts[:n] == '1' ? 'NOT' : ''
78
+ else
79
+ Wice.log "invalid parameters for the grid string filter - must be a string: #{opts.inspect} or a Hash with keys :v and :n"
80
+ return false
81
+ end
82
+ if string_fragment.empty?
83
+ Wice.log "invalid parameters for the grid string filter - empty string"
84
+ return false
85
+ end
86
+ @criteria.where(@field.name.to_s => /#{string_fragment}/)
87
+ return true
88
+ end
89
+
90
+ end
91
+
92
+ class FilterConditionsGeneratorInteger < FilterConditionsGenerator #:nodoc:
93
+ @@handled_type[Integer] = self
94
+ # @@handled_type[Float] = self
95
+ # @@handled_type[BigDecimal] = self
96
+
97
+ def generate_conditions(opts) #:nodoc:
98
+ unless opts.kind_of? Hash
99
+ Wice.log "invalid parameters for the grid integer filter - must be a hash"
100
+ return false
101
+ end
102
+
103
+ if !opts[:fr] || !(opts[:fr] =~ /\d/) || !opts[:to] || !(opts[:to] =~ /\d/)
104
+ Wice.log "invalid parameters for the grid integer filter - either range limits are not supplied or they are not numeric"
105
+ return false
106
+ end
107
+
108
+ @criteria.where(@field.name.to_sym.gt => opts[:fr].to_i)
109
+ @criteria.where(@field.name.to_sym.lt => opts[:to].to_i)
110
+
111
+ return true
112
+ end
113
+ end
114
+
115
+ class FilterConditionsGeneratorDate < FilterConditionsGenerator #:nodoc:
116
+ @@handled_type[Date] = self
117
+ @@handled_type[DateTime] = self
118
+ @@handled_type[Time] = self
119
+
120
+ def generate_conditions(opts) #:nodoc:
121
+ @criteria.where(@field.name.to_sym.gt => opts[:fr]) if opts[:fr]
122
+ @criteria.where(@field.name.to_sym.lt => opts[:to]) if opts[:to]
123
+ opts[:fr] || opts[:to]
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,107 @@
1
+
2
+ div.calendar
3
+ {
4
+ font-size: smaller;
5
+ color: #000;
6
+ z-index: 5;
7
+ }
8
+
9
+ div.calendar.popup
10
+ {
11
+ margin-left: -40px;
12
+ margin-top: -100px;
13
+ }
14
+
15
+ div.calendar table
16
+ {
17
+ background-color: #eee;
18
+ border: 1px solid #aaa;
19
+ border-collapse: collapse;
20
+ }
21
+
22
+ div.calendar thead {
23
+ background-color: white;
24
+ }
25
+
26
+ div.calendar td,
27
+ div.calendar th
28
+ {
29
+ padding: 3px;
30
+ text-align: center;
31
+ }
32
+
33
+ div.calendar td.title
34
+ {
35
+ font-weight: bold;
36
+ }
37
+
38
+ div.calendar th
39
+ {
40
+ background: #ddd;
41
+ border-bottom: 1px solid #ccc;
42
+ border-top: 1px solid #ccc;
43
+ font-weight: bold;
44
+ color: #555;
45
+ }
46
+
47
+ div.calendar tr.days td {
48
+ width: 2em;
49
+ color: #555;
50
+ text-align: center;
51
+ cursor: pointer;
52
+ }
53
+
54
+ div.calendar tr.days td:hover,
55
+ div.calendar td.cvbutton:hover
56
+ {
57
+ background-color: #34ABFA;
58
+ cursor: pointer;
59
+ }
60
+
61
+
62
+ div.calendar tr td.closeButton:hover
63
+ {
64
+ background-color: #34ABFA;
65
+ cursor: pointer;
66
+ }
67
+
68
+
69
+ div.calendar tr.days td:active
70
+ div.calendar td.cvbutton:active
71
+ {
72
+ background-color: #cde;
73
+ }
74
+
75
+ div.calendar tr.days td.selected
76
+ {
77
+ font-weight: bold;
78
+ background-color: #fff;
79
+ color: #000;
80
+ }
81
+
82
+ div.calendar tr.days td.today
83
+ {
84
+ font-weight: bold;
85
+ color: #D50000;
86
+ }
87
+
88
+ div.calendar tr.days td.otherDay
89
+ {
90
+ color: #bbb;
91
+ }
92
+
93
+ div.calendar .draggableHandler{
94
+ cursor: move;
95
+ }
96
+
97
+ /* styles for the date_picker Rails plugin */
98
+ span.date_picker a.date_label{
99
+ margin-left: 5px;
100
+ margin-right: 5px;
101
+ text-decoration: none;
102
+ }
103
+ span.date_picker a.date_label:hover{text-decoration: line-through;}
104
+
105
+ span.date_picker span.trigger:hover{
106
+ cursor: pointer;
107
+ }
@@ -0,0 +1,1168 @@
1
+ //
2
+ // CalendarView (for Prototype)
3
+ // calendarview.org
4
+ //
5
+ // Maintained by Justin Mecham <justin@aspect.net>
6
+ //
7
+ // Portions Copyright 2002-2005 Mihai Bazon
8
+ //
9
+ // This calendar is based very loosely on the Dynarch Calendar in that it was
10
+ // used as a base, but completely gutted and more or less rewritten in place
11
+ // to use the Prototype JavaScript library.
12
+ //
13
+ // As such, CalendarView is licensed under the terms of the GNU Lesser General
14
+ // Public License (LGPL). More information on the Dynarch Calendar can be
15
+ // found at:
16
+ //
17
+ // www.dynarch.com/projects/calendar
18
+ //
19
+
20
+
21
+ /* This fork by Yuri Leikind ( git://github.com/leikind/calendarview.git ) adds a number of features.
22
+
23
+ The differences from the original are
24
+
25
+ * Support for time in the form of two dropdowns for hours and minutes. Can be turned off/on.
26
+ * Draggable popup calendars (which introduces new dependancies: script.aculo.us effects.js and dragdrop.js)
27
+ * Close button
28
+ * Ability to unset the date by clicking on the active date
29
+ * Simple I18n support
30
+ * Removed all ambiguity in the API
31
+ * Two strategies in positioning of popup calendars: relative to the popup trigger element (original behavior),
32
+ and is relative to the mouse pointer (can be configured)
33
+ * Popup calendars are not created every time they pop up, on the contrary, they are created once just like
34
+ embedded calendars, and then shown or hidden.
35
+ * Possible to have many popup calendars on page. The behavior of the original calendarview when a popup
36
+ calendar is hidden when the user clicks elsewhere on the page is an option now.
37
+ * Refactoring and changes to the OO design like getting rid of Calendar.prototype in favor of class based
38
+ OO provided by OO, and getting rid of Calendar.setup({}) in favor of a simple object constructor new Calendar({}).
39
+
40
+ */
41
+
42
+ var Calendar = Class.create({
43
+
44
+ container: null,
45
+
46
+ minYear: 1900,
47
+ maxYear: 2100,
48
+
49
+ date: new Date(),
50
+ currentDateElement: null,
51
+
52
+ shouldClose: false,
53
+ isPopup: true,
54
+
55
+ initialize: function(params){
56
+
57
+ if (! Calendar.init_done){
58
+ Calendar.init();
59
+ }
60
+
61
+ embedAt = params.embedAt || null;
62
+ withTime = params.withTime || null;
63
+ dateFormat = params.dateFormat || null;
64
+ initialDate = params.initialDate || null;
65
+ popupTriggerElement = params.popupTriggerElement || null;
66
+ this.onHideCallback = params.onHideCallback || function(date, calendar){};
67
+ this.onDateChangedCallback = params.onDateChangedCallback || function(date, calendar){};
68
+ this.minuteStep = params.minuteStep || 5;
69
+ this.hideOnClickOnDay = params.hideOnClickOnDay || false;
70
+ this.hideOnClickElsewhere = params.hideOnClickElsewhere || false;
71
+ this.outputFields = params.outputFields || $A();
72
+ this.popupPositioningStrategy = params.popupPositioningStrategy || 'trigger'; // or 'pointer'
73
+ this.x = params.x || 0;
74
+ this.y = params.y || 0.6;
75
+
76
+ this.outputFields = $A(this.outputFields).collect(function(f){
77
+ return $(f);
78
+ });
79
+
80
+ if (embedAt){
81
+ this.embedAt = $(embedAt);
82
+ this.embedAt._calendar = this;
83
+ }else{
84
+ this.embedAt = null;
85
+ }
86
+
87
+ this.withTime = withTime;
88
+
89
+ if (dateFormat){
90
+ this.dateFormat = dateFormat;
91
+ }else{
92
+ if(this.withTime){
93
+ this.dateFormat = Calendar.defaultDateTimeFormat;
94
+ }else{
95
+ this.dateFormat = Calendar.defaultDateFormat;
96
+ }
97
+ }
98
+
99
+ this.dateFormatForHiddenField = params.dateFormatForHiddenField || this.dateFormat;
100
+
101
+
102
+ if (initialDate) {
103
+ this.date = this.parseDate(initialDate);
104
+ }
105
+
106
+ this.build();
107
+
108
+ if (this.isPopup) { //Popup Calendars
109
+ var popupTriggerElement = $(popupTriggerElement);
110
+ popupTriggerElement._calendar = this;
111
+
112
+ popupTriggerElement.observe('click', function(event){
113
+ this.showAtElement(event, popupTriggerElement);
114
+ }.bind(this) );
115
+
116
+ } else{ // In-Page Calendar
117
+ this.show();
118
+ }
119
+
120
+ if (params.updateOuterFieldsOnInit){
121
+ this.updateOuterFieldWithoutCallback(); // Just for the sake of localization and DatePicker
122
+ }
123
+ },
124
+
125
+ build: function(){
126
+ if (this.embedAt) {
127
+ var parentForCalendarTable = this.embedAt;
128
+ this.isPopup = false;
129
+ } else {
130
+ var parentForCalendarTable = document.getElementsByTagName('body')[0];
131
+ this.isPopup = true;
132
+ }
133
+
134
+
135
+ var table = new Element('table');
136
+
137
+ var thead = new Element('thead');
138
+ table.appendChild(thead);
139
+
140
+ var firstRow = new Element('tr');
141
+
142
+ if (this.isPopup){
143
+ var cell = new Element('td');
144
+ cell.addClassName('draggableHandler');
145
+ firstRow.appendChild(cell);
146
+
147
+ cell = new Element('td', { colSpan: 5 });
148
+ cell.addClassName('title' );
149
+ cell.addClassName('draggableHandler');
150
+ firstRow.appendChild(cell);
151
+
152
+ cell = new Element('td');
153
+ cell.addClassName('closeButton');
154
+ firstRow.appendChild(cell);
155
+ cell.update('x');
156
+
157
+ cell.observe('mousedown', function(){
158
+ this.hide();
159
+ }.bind(this));
160
+
161
+
162
+ }else{
163
+ var cell = new Element('td', { colSpan: 7 } );
164
+ firstRow.appendChild(cell);
165
+ }
166
+
167
+ cell.addClassName('title');
168
+
169
+ thead.appendChild(firstRow);
170
+
171
+ var row = new Element('tr')
172
+ this._drawButtonCell(row, '&#x00ab;', 1, Calendar.NAV_PREVIOUS_YEAR);
173
+ this._drawButtonCell(row, '&#x2039;', 1, Calendar.NAV_PREVIOUS_MONTH);
174
+ this._drawButtonCell(row, Calendar.getMessageFor('today'), 3, Calendar.NAV_TODAY);
175
+ this._drawButtonCell(row, '&#x203a;', 1, Calendar.NAV_NEXT_MONTH);
176
+ this._drawButtonCell(row, '&#x00bb;', 1, Calendar.NAV_NEXT_YEAR);
177
+ thead.appendChild(row)
178
+
179
+ // Day Names
180
+ row = new Element('tr');
181
+ for (var i = 0; i < 7; ++i) {
182
+ cell = new Element('th').update(Calendar.SHORT_DAY_NAMES[i]);
183
+ if (i == 0 || i == 6){
184
+ cell.addClassName('weekend');
185
+ }
186
+ row.appendChild(cell);
187
+ }
188
+ thead.appendChild(row);
189
+
190
+ // Calendar Days
191
+ var tbody = table.appendChild(new Element('tbody'));
192
+ for (i = 6; i > 0; --i) {
193
+ row = tbody.appendChild(new Element('tr'));
194
+ row.addClassName('days');
195
+ for (var j = 7; j > 0; --j) {
196
+ cell = row.appendChild(new Element('td'));
197
+ cell.calendar = this;
198
+ }
199
+ }
200
+
201
+ // Time Placeholder
202
+ if (this.withTime){
203
+ var tfoot = table.appendChild(new Element('tfoot'));
204
+ row = tfoot.appendChild(new Element('tr'));
205
+ cell = row.appendChild(new Element('td', { colSpan: 7 }));
206
+ cell.addClassName('time');
207
+ var hourSelect = cell.appendChild(new Element('select', { name : 'hourSelect'}));
208
+ for (var i = 0; i < 24; i++) {
209
+ hourSelect.appendChild(new Element('option', {value : i}).update(i));
210
+ }
211
+ this.hourSelect = hourSelect;
212
+
213
+ cell.appendChild(new Element('span')).update(' : ');
214
+
215
+ var minuteSelect = cell.appendChild(new Element('select', { name : 'minuteSelect'}));
216
+ for (var i = 0; i < 60; i += this.minuteStep) {
217
+ minuteSelect.appendChild(new Element('option', {value : i}).update(i));
218
+ }
219
+ this.minuteSelect = minuteSelect;
220
+
221
+ hourSelect.observe('change', function(event){
222
+ if (! this.date) return;
223
+ var elem = event.element();
224
+ var selectedIndex = elem.selectedIndex;
225
+ if ((typeof selectedIndex != 'undefined') && selectedIndex != null){
226
+ this.date.setHours(elem.options[selectedIndex].value);
227
+ this.updateOuterField();
228
+ }
229
+ }.bind(this));
230
+
231
+ minuteSelect.observe('change', function(event){
232
+ if (! this.date) return;
233
+ var elem = event.element();
234
+ var selectedIndex = elem.selectedIndex;
235
+ if ((typeof selectedIndex != 'undefined') && selectedIndex != null){
236
+ this.date.setMinutes(elem.options[selectedIndex].value);
237
+ this.updateOuterField();
238
+ }
239
+ }.bind(this))
240
+ }
241
+
242
+ // Calendar Container (div)
243
+ this.container = new Element('div');
244
+ this.container.addClassName('calendar');
245
+ if (this.isPopup) {
246
+ this.container.setStyle({ position: 'absolute', display: 'none' });
247
+ this.container.addClassName('popup');
248
+ }
249
+ this.container.appendChild(table);
250
+
251
+ this.update(this.date);
252
+
253
+ Event.observe(this.container, 'mousedown', Calendar.handleMouseDownEvent);
254
+
255
+ parentForCalendarTable.appendChild(this.container);
256
+
257
+ if (this.isPopup){
258
+ new Draggable(table, {handle : firstRow });
259
+ }
260
+ },
261
+
262
+ updateOuterFieldReal: function(element){
263
+ if (element.tagName == 'DIV' || element.tagName == 'SPAN') {
264
+ formatted = this.date ? this.date.print(this.dateFormat) : '';
265
+ element.update(formatted);
266
+ } else if (element.tagName == 'INPUT') {
267
+ formatted = this.date ? this.date.print(this.dateFormatForHiddenField) : '';
268
+ element.value = formatted;
269
+ }
270
+ },
271
+
272
+ updateOuterFieldWithoutCallback: function(){
273
+ this.outputFields.each(function(field){
274
+ this.updateOuterFieldReal(field);
275
+ }.bind(this));
276
+ },
277
+
278
+ updateOuterField: function(){
279
+ this.updateOuterFieldWithoutCallback();
280
+ this.onDateChangedCallback(this.date, this);
281
+ },
282
+
283
+ viewOutputFields: function(){
284
+ return this.outputFields.findAll(function(element){
285
+ if (element.tagName == 'DIV' || element.tagName == 'SPAN'){
286
+ return true;
287
+ }else{
288
+ return false;
289
+ }
290
+ });
291
+ },
292
+
293
+
294
+ //----------------------------------------------------------------------------
295
+ // Update Calendar
296
+ //----------------------------------------------------------------------------
297
+
298
+ update: function(date) {
299
+
300
+ var today = new Date();
301
+ var thisYear = today.getFullYear();
302
+ var thisMonth = today.getMonth();
303
+ var thisDay = today.getDate();
304
+ var month = date.getMonth();
305
+ var dayOfMonth = date.getDate();
306
+ var hour = date.getHours();
307
+ var minute = date.getMinutes();
308
+
309
+ // Ensure date is within the defined range
310
+ if (date.getFullYear() < this.minYear)
311
+ date.__setFullYear(this.minYear);
312
+ else if (date.getFullYear() > this.maxYear)
313
+ date.__setFullYear(this.maxYear);
314
+
315
+ if (this.isBackedUp()){
316
+ this.dateBackedUp = new Date(date);
317
+ }else{
318
+ this.date = new Date(date);
319
+ }
320
+
321
+ // Calculate the first day to display (including the previous month)
322
+ date.setDate(1)
323
+ date.setDate(-(date.getDay()) + 1)
324
+
325
+ // Fill in the days of the month
326
+ Element.getElementsBySelector(this.container, 'tbody tr').each(
327
+ function(row, i) {
328
+ var rowHasDays = false;
329
+ row.immediateDescendants().each(
330
+ function(cell, j) {
331
+ var day = date.getDate();
332
+ var dayOfWeek = date.getDay();
333
+ var isCurrentMonth = (date.getMonth() == month);
334
+
335
+ // Reset classes on the cell
336
+ cell.className = '';
337
+ cell.date = new Date(date);
338
+ cell.update(day);
339
+
340
+ // Account for days of the month other than the current month
341
+ if (!isCurrentMonth)
342
+ cell.addClassName('otherDay');
343
+ else
344
+ rowHasDays = true;
345
+
346
+ // Ensure the current day is selected
347
+ if ((! this.isBackedUp()) && isCurrentMonth && day == dayOfMonth) {
348
+ cell.addClassName('selected');
349
+ this.currentDateElement = cell;
350
+ }
351
+
352
+ // Today
353
+ if (date.getFullYear() == thisYear && date.getMonth() == thisMonth && day == thisDay)
354
+ cell.addClassName('today');
355
+
356
+ // Weekend
357
+ if ([0, 6].indexOf(dayOfWeek) != -1)
358
+ cell.addClassName('weekend');
359
+
360
+ // Set the date to tommorrow
361
+ date.setDate(day + 1);
362
+ }.bind(this)
363
+ )
364
+ // Hide the extra row if it contains only days from another month
365
+ !rowHasDays ? row.hide() : row.show();
366
+ }.bind(this)
367
+ )
368
+
369
+ Element.getElementsBySelector(this.container, 'tfoot tr td select').each(
370
+ function(sel){
371
+ if(sel.name == 'hourSelect'){
372
+ sel.selectedIndex = hour;
373
+ }else if(sel.name == 'minuteSelect'){
374
+ if (this.minuteStep == 1){
375
+ sel.selectedIndex = minute;
376
+ }else{
377
+ sel.selectedIndex = this.findClosestMinute(minute);
378
+ }
379
+ }
380
+ }.bind(this)
381
+ )
382
+
383
+ this.container.getElementsBySelector('td.title')[0].update(
384
+ Calendar.MONTH_NAMES[month] + ' ' + this.dateOrDateBackedUp().getFullYear()
385
+ )
386
+
387
+ },
388
+
389
+
390
+ findClosestMinute: function(val){
391
+ if (val == 0){
392
+ return 0;
393
+ }
394
+ var lowest = ((val / this.minuteStep).floor() * this.minuteStep);
395
+ var distance = val % this.minuteStep;
396
+ var minuteValueToShow;
397
+
398
+ if (distance <= (this.minuteStep / 2)){
399
+ minuteValueToShow = lowest;
400
+ }else{
401
+ minuteValueToShow = lowest + this.minuteStep;
402
+ }
403
+
404
+ if (minuteValueToShow == 0){
405
+ return minuteValueToShow;
406
+ }else if(minuteValueToShow >= 60){
407
+ return (minuteValueToShow / this.minuteStep).floor() - 1;
408
+ }else{
409
+ return minuteValueToShow / this.minuteStep;
410
+ }
411
+ },
412
+
413
+ _drawButtonCell: function(parentForCell, text, colSpan, navAction) {
414
+ var cell = new Element('td');
415
+ if (colSpan > 1) cell.colSpan = colSpan;
416
+ cell.className = 'cvbutton';
417
+ cell.calendar = this;
418
+ cell.navAction = navAction;
419
+ cell.innerHTML = text;
420
+ cell.unselectable = 'on'; // IE
421
+ parentForCell.appendChild(cell);
422
+ return cell;
423
+ },
424
+
425
+
426
+ //------------------------------------------------------------------------------
427
+ // Calendar Display Functions
428
+ //------------------------------------------------------------------------------
429
+
430
+ show: function(){
431
+ this.container.show();
432
+ if (this.isPopup) {
433
+ if (this.hideOnClickElsewhere){
434
+ window._popupCalendar = this;
435
+ document.observe('mousedown', Calendar._checkCalendar);
436
+ }
437
+ }
438
+ },
439
+
440
+ showAt: function (x, y) {
441
+ this.container.setStyle({ left: x + 'px', top: y + 'px' });
442
+ this.show();
443
+ },
444
+
445
+
446
+ showAtElement: function(event, element) {
447
+ this.container.show();
448
+ var x, y;
449
+ if (this.popupPositioningStrategy == 'pointer'){ // follow the mouse pointer
450
+ var pos = Event.pointer(event);
451
+ var containerWidth = this.container.getWidth();
452
+ x = containerWidth * this.x + pos.x;
453
+ y = containerWidth * this.y + pos.y;
454
+ }else{ // 'container' - container of the trigger elements
455
+ var pos = Position.cumulativeOffset(element);
456
+ x = pos[0];
457
+ y = this.container.offsetHeight * 0.75 + pos[1];
458
+ }
459
+ this.showAt(x, y);
460
+ },
461
+
462
+ hide: function() {
463
+ if (this.isPopup){
464
+ Event.stopObserving(document, 'mousedown', Calendar._checkCalendar);
465
+ }
466
+ this.container.hide();
467
+ this.onHideCallback(this.date, this);
468
+ },
469
+
470
+
471
+ // Tries to identify the date represented in a string. If successful it also
472
+ // calls this.updateIfDateDifferent which moves the calendar to the given date.
473
+ parseDate: function(str, format){
474
+ if (!format){
475
+ format = this.dateFormat;
476
+ }
477
+ var res = Date.parseDate(str, format);
478
+ return res;
479
+ },
480
+
481
+
482
+ dateOrDateBackedUp: function(){
483
+ return this.date || this.dateBackedUp;
484
+ },
485
+
486
+ updateIfDateDifferent: function(date) {
487
+ if (!date.equalsTo(this.dateOrDateBackedUp())){
488
+ this.update(date);
489
+ }
490
+ },
491
+
492
+ backupDateAndCurrentElement: function(){
493
+ if (this.minuteSelect){
494
+ this.minuteSelect.disable();
495
+ }
496
+ if (this.hourSelect){
497
+ this.hourSelect.disable();
498
+ }
499
+
500
+ this.currentDateElementBackedUp = this.currentDateElement;
501
+ this.currentDateElement = null;
502
+
503
+ this.dateBackedUp = this.date;
504
+ this.date = null;
505
+ },
506
+
507
+ restoreDateAndCurrentElement: function(){
508
+ if (this.minuteSelect){
509
+ this.minuteSelect.enable();
510
+ }
511
+ if (this.hourSelect){
512
+ this.hourSelect.enable();
513
+ }
514
+
515
+ this.currentDateElement = this.currentDateElementBackedUp;
516
+ this.currentDateElementBackedUp = null;
517
+
518
+ this.date = this.dateBackedUp;
519
+ this.dateBackedUp = null;
520
+ },
521
+
522
+ isBackedUp: function(){
523
+ return ((this.date == null) && this.dateBackedUp);
524
+ },
525
+
526
+ dumpDates: function(){
527
+ console.log('date: ' + this.date);
528
+ console.log('dateBackedUp: ' + this.dateBackedUp);
529
+ },
530
+
531
+
532
+ setRange: function(minYear, maxYear) {
533
+ this.minYear = minYear;
534
+ this.maxYear = maxYear;
535
+ }
536
+ })
537
+
538
+ // Delete or add new locales from I18n.js according to your needs
539
+ Calendar.messagebundle = $H({'en' :
540
+ $H({
541
+ 'monday' : 'Monday',
542
+ 'tuesday' : 'Tuesday',
543
+ 'wednesday' : 'Wednesday',
544
+ 'thursday' : 'Thursday',
545
+ 'friday' : 'Friday',
546
+ 'saturday' : 'Saturday',
547
+ 'sunday' : 'Sunday',
548
+
549
+ 'monday_short' : 'M',
550
+ 'tuesday_short' : 'T',
551
+ 'wednesday_short' : 'W',
552
+ 'thursday_short' : 'T',
553
+ 'friday_short' : 'F',
554
+ 'saturday_short' : 'S',
555
+ 'sunday_short' : 'S',
556
+
557
+ 'january' : 'January',
558
+ 'february' : 'February',
559
+ 'march' : 'March',
560
+ 'april' : 'April',
561
+ 'may' : 'May',
562
+ 'june' : 'June',
563
+ 'july' : 'July',
564
+ 'august' : 'August',
565
+ 'september' : 'September',
566
+ 'october' : 'October',
567
+ 'november' : 'November',
568
+ 'december' : 'December',
569
+
570
+ 'january_short' : 'Jan',
571
+ 'february_short' : 'Feb',
572
+ 'march_short' : 'Mar',
573
+ 'april_short' : 'Apr',
574
+ 'may_short' : 'May',
575
+ 'june_short' : 'Jun',
576
+ 'july_short' : 'Jul',
577
+ 'august_short' : 'Aug',
578
+ 'september_short' : 'Sep',
579
+ 'october_short' : 'Oct',
580
+ 'november_short' : 'Nov',
581
+ 'december_short' : 'Dec',
582
+
583
+ 'today' : 'Today'
584
+ }),
585
+ 'fr' :
586
+ $H({
587
+ 'monday' : 'Lundi',
588
+ 'tuesday' : 'Mardi',
589
+ 'wednesday' : 'Mercredi',
590
+ 'thursday' : 'Jeudi',
591
+ 'friday' : 'Vendredi',
592
+ 'saturday' : 'Samedi',
593
+ 'sunday' : 'Dimanche',
594
+
595
+ 'monday_short' : 'Lu',
596
+ 'tuesday_short' : 'Ma',
597
+ 'wednesday_short' : 'Me',
598
+ 'thursday_short' : 'Je',
599
+ 'friday_short' : 'Ve',
600
+ 'saturday_short' : 'Sa',
601
+ 'sunday_short' : 'Di',
602
+
603
+ 'january' : 'janvier',
604
+ 'february' : 'février',
605
+ 'march' : 'mars',
606
+ 'april' : 'avril',
607
+ 'may' : 'mai',
608
+ 'june' : 'juin',
609
+ 'july' : 'juillet',
610
+ 'august' : 'août',
611
+ 'september' : 'septembre',
612
+ 'october' : 'octobre',
613
+ 'november' : 'novembre',
614
+ 'december' : 'décembre',
615
+
616
+ 'january_short' : 'jan',
617
+ 'february_short' : 'fév',
618
+ 'march_short' : 'mar',
619
+ 'april_short' : 'avr',
620
+ 'may_short' : 'mai',
621
+ 'june_short' : 'jun',
622
+ 'july_short' : 'jul',
623
+ 'august_short' : 'aoû',
624
+ 'september_short' : 'sep',
625
+ 'october_short' : 'oct',
626
+ 'november_short' : 'nov',
627
+ 'december_short' : 'dec',
628
+
629
+ 'today' : 'aujourd\'hui'
630
+ }),
631
+ 'nl' :
632
+ $H({
633
+ 'monday' : 'maandag',
634
+ 'tuesday' : 'dinsdag',
635
+ 'wednesday' : 'woensdag',
636
+ 'thursday' : 'donderdag',
637
+ 'friday' : 'vrijdag',
638
+ 'saturday' : 'zaterdag',
639
+ 'sunday' : 'zondag',
640
+
641
+ 'monday_short' : 'Ma',
642
+ 'tuesday_short' : 'Di',
643
+ 'wednesday_short' : 'Wo',
644
+ 'thursday_short' : 'Do',
645
+ 'friday_short' : 'Vr',
646
+ 'saturday_short' : 'Za',
647
+ 'sunday_short' : 'Zo',
648
+
649
+ 'january' : 'januari',
650
+ 'february' : 'februari',
651
+ 'march' : 'maart',
652
+ 'april' : 'april',
653
+ 'may' : 'mei',
654
+ 'june' : 'juni',
655
+ 'july' : 'juli',
656
+ 'august' : 'augustus',
657
+ 'september' : 'september',
658
+ 'october' : 'oktober',
659
+ 'november' : 'november',
660
+ 'december' : 'december',
661
+
662
+ 'january_short' : 'jan',
663
+ 'february_short' : 'feb',
664
+ 'march_short' : 'mrt',
665
+ 'april_short' : 'apr',
666
+ 'may_short' : 'mei',
667
+ 'june_short' : 'jun',
668
+ 'july_short' : 'jul',
669
+ 'august_short' : 'aug',
670
+ 'september_short' : 'sep',
671
+ 'october_short' : 'okt',
672
+ 'november_short' : 'nov',
673
+ 'december_short' : 'dec',
674
+
675
+ 'today' : 'vandaag'
676
+ })
677
+ });
678
+
679
+
680
+ Calendar.getMessageFor = function(key){
681
+
682
+ var lang = Calendar.language || 'en';
683
+ if (! Calendar.messagebundle.get(lang)){
684
+ lang = 'en';
685
+ }
686
+ return Calendar.messagebundle.get(lang).get(key);
687
+ };
688
+
689
+ Calendar.VERSION = '1.4';
690
+
691
+ Calendar.defaultDateFormat = '%Y-%m-%d';
692
+ Calendar.defaultDateTimeFormat = '%Y-%m-%d %H:%M';
693
+
694
+ // we need to postpone the initialization of these structures to let the page define the language of the page
695
+ Calendar.init = function(){
696
+
697
+ Calendar.DAY_NAMES = new Array(
698
+ Calendar.getMessageFor('sunday'),
699
+ Calendar.getMessageFor('monday'),
700
+ Calendar.getMessageFor('tuesday'),
701
+ Calendar.getMessageFor('wednesday'),
702
+ Calendar.getMessageFor('thursday'),
703
+ Calendar.getMessageFor('friday'),
704
+ Calendar.getMessageFor('saturday')
705
+ );
706
+
707
+ Calendar.SHORT_DAY_NAMES = new Array(
708
+ Calendar.getMessageFor('sunday_short'),
709
+ Calendar.getMessageFor('monday_short'),
710
+ Calendar.getMessageFor('tuesday_short'),
711
+ Calendar.getMessageFor('wednesday_short'),
712
+ Calendar.getMessageFor('thursday_short'),
713
+ Calendar.getMessageFor('friday_short'),
714
+ Calendar.getMessageFor('saturday_short')
715
+ );
716
+
717
+ Calendar.MONTH_NAMES = new Array(
718
+ Calendar.getMessageFor('january'),
719
+ Calendar.getMessageFor('february'),
720
+ Calendar.getMessageFor('march'),
721
+ Calendar.getMessageFor('april'),
722
+ Calendar.getMessageFor('may'),
723
+ Calendar.getMessageFor('june'),
724
+ Calendar.getMessageFor('july'),
725
+ Calendar.getMessageFor('august'),
726
+ Calendar.getMessageFor('september'),
727
+ Calendar.getMessageFor('october'),
728
+ Calendar.getMessageFor('november'),
729
+ Calendar.getMessageFor('december')
730
+ );
731
+
732
+ Calendar.SHORT_MONTH_NAMES = new Array(
733
+ Calendar.getMessageFor('january_short'),
734
+ Calendar.getMessageFor('february_short'),
735
+ Calendar.getMessageFor('march_short'),
736
+ Calendar.getMessageFor('april_short'),
737
+ Calendar.getMessageFor('may_short'),
738
+ Calendar.getMessageFor('june_short'),
739
+ Calendar.getMessageFor('july_short'),
740
+ Calendar.getMessageFor('august_short'),
741
+ Calendar.getMessageFor('september_short'),
742
+ Calendar.getMessageFor('october_short'),
743
+ Calendar.getMessageFor('november_short'),
744
+ Calendar.getMessageFor('december_short')
745
+ );
746
+ Calendar.init_done = true;
747
+ };
748
+
749
+ Calendar.NAV_PREVIOUS_YEAR = -2;
750
+ Calendar.NAV_PREVIOUS_MONTH = -1;
751
+ Calendar.NAV_TODAY = 0;
752
+ Calendar.NAV_NEXT_MONTH = 1;
753
+ Calendar.NAV_NEXT_YEAR = 2;
754
+
755
+ //------------------------------------------------------------------------------
756
+ // Static Methods
757
+ //------------------------------------------------------------------------------
758
+
759
+ // This gets called when the user presses a mouse button anywhere in the
760
+ // document, if the calendar is shown. If the click was outside the open
761
+ // calendar this function closes it.
762
+ Calendar._checkCalendar = function(event) {
763
+ if (!window._popupCalendar){
764
+ return false;
765
+ }
766
+ if (Element.descendantOf(Event.element(event), window._popupCalendar.container)){
767
+ return;
768
+ }
769
+ Calendar.closeHandler(window._popupCalendar);
770
+ return Event.stop(event);
771
+ }
772
+
773
+ //------------------------------------------------------------------------------
774
+ // Event Handlers
775
+ //------------------------------------------------------------------------------
776
+
777
+ Calendar.handleMouseDownEvent = function(event){
778
+ if (event.element().type == 'select-one'){ // ignore select elements - not escaping this in Safari leaves select boxes non-functional
779
+ return true;
780
+ }
781
+ Event.observe(document, 'mouseup', Calendar.handleMouseUpEvent);
782
+ Event.stop(event)
783
+ }
784
+
785
+ Calendar.handleMouseUpEvent = function(event){
786
+ var el = Event.element(event);
787
+ var calendar = el.calendar;
788
+ var isNewDate = false;
789
+
790
+
791
+ // If the element that was clicked on does not have an associated Calendar
792
+ // object, return as we have nothing to do.
793
+ if (!calendar) return false;
794
+
795
+ // Clicked on a day
796
+ if (typeof el.navAction == 'undefined') {
797
+
798
+ var dateWasDefined = true;
799
+ if (calendar.date == null){
800
+ dateWasDefined = false;
801
+ calendar.restoreDateAndCurrentElement();
802
+ }
803
+
804
+
805
+ if (calendar.currentDateElement) {
806
+ Element.removeClassName(calendar.currentDateElement, 'selected');
807
+
808
+ if (dateWasDefined && el == calendar.currentDateElement){
809
+ calendar.backupDateAndCurrentElement();
810
+
811
+ calendar.updateOuterField();
812
+
813
+ Event.stopObserving(document, 'mouseup', Calendar.handleMouseUpEvent);
814
+ return Event.stop(event);
815
+ }
816
+
817
+ Element.addClassName(el, 'selected');
818
+
819
+ calendar.shouldClose = (calendar.currentDateElement == el);
820
+
821
+ if (!calendar.shouldClose) {
822
+
823
+ calendar.currentDateElement = el;
824
+ }
825
+ }
826
+ calendar.date.setDateOnly(el.date);
827
+ isNewDate = true;
828
+
829
+ calendar.shouldClose = !el.hasClassName('otherDay');
830
+
831
+
832
+ var isOtherMonth = !calendar.shouldClose;
833
+ if (isOtherMonth) {
834
+ calendar.update(calendar.date);
835
+ }
836
+
837
+ if (! calendar.hideOnClickOnDay){ // override closing if calendar.hideOnClickOnDay is false
838
+ calendar.shouldClose = false;
839
+ }
840
+
841
+ } else { // Clicked on an action button
842
+
843
+ var date = new Date(calendar.dateOrDateBackedUp());
844
+
845
+ if (el.navAction == Calendar.NAV_TODAY){
846
+ date.setDateOnly(new Date());
847
+ }
848
+
849
+ var year = date.getFullYear();
850
+ var mon = date.getMonth();
851
+
852
+ function setMonth(m) {
853
+ var day = date.getDate();
854
+ var max = date.getMonthDays(m);
855
+ if (day > max) date.setDate(max);
856
+ date.setMonth(m);
857
+ }
858
+
859
+ switch (el.navAction) {
860
+
861
+ // Previous Year
862
+ case Calendar.NAV_PREVIOUS_YEAR:
863
+ if (year > calendar.minYear)
864
+ date.__setFullYear(year - 1);
865
+ break;
866
+
867
+ // Previous Month
868
+ case Calendar.NAV_PREVIOUS_MONTH:
869
+ if (mon > 0) {
870
+ setMonth(mon - 1);
871
+ }
872
+ else if (year-- > calendar.minYear) {
873
+ date.__setFullYear(year);
874
+ setMonth(11);
875
+ }
876
+ break;
877
+
878
+ // Today
879
+ case Calendar.NAV_TODAY:
880
+ break;
881
+
882
+ // Next Month
883
+ case Calendar.NAV_NEXT_MONTH:
884
+ if (mon < 11) {
885
+ setMonth(mon + 1);
886
+ }else if (year < calendar.maxYear) {
887
+ date.__setFullYear(year + 1);
888
+ setMonth(0);
889
+ }
890
+ break;
891
+
892
+ // Next Year
893
+ case Calendar.NAV_NEXT_YEAR:
894
+ if (year < calendar.maxYear){
895
+ date.__setFullYear(year + 1);
896
+ }
897
+ break;
898
+ }
899
+
900
+ if (!date.equalsTo(calendar.dateOrDateBackedUp())) {
901
+ calendar.updateIfDateDifferent(date);
902
+ isNewDate = true;
903
+ } // else if (el.navAction == 0) {
904
+ // isNewDate = (calendar.shouldClose = true);
905
+ // } // Hm, what did I mean with this code?
906
+ }
907
+
908
+ if (isNewDate && event) {
909
+ Calendar.selectHandler(calendar);
910
+ }
911
+
912
+ if (calendar.shouldClose && event) {
913
+ Calendar.closeHandler(calendar);
914
+ }
915
+
916
+ Event.stopObserving(document, 'mouseup', Calendar.handleMouseUpEvent);
917
+ return Event.stop(event);
918
+ }
919
+
920
+ Calendar.selectHandler = function(calendar){
921
+
922
+ // Update dateField value
923
+ calendar.updateOuterField();
924
+
925
+
926
+ // Call the close handler, if necessary
927
+ if (calendar.shouldClose) {
928
+ Calendar.closeHandler(calendar);
929
+ }
930
+ }
931
+
932
+ Calendar.closeHandler = function(calendar){
933
+ calendar.hide();
934
+ calendar.shouldClose = false;
935
+ }
936
+
937
+
938
+
939
+ // global object that remembers the calendar
940
+ window._popupCalendar = null;
941
+
942
+
943
+ //==============================================================================
944
+ //
945
+ // Date Object Patches
946
+ //
947
+ // This is pretty much untouched from the original. I really would like to get
948
+ // rid of these patches if at all possible and find a cleaner way of
949
+ // accomplishing the same things. It's a shame Prototype doesn't extend Date at
950
+ // all.
951
+ //
952
+ //==============================================================================
953
+
954
+ Date.DAYS_IN_MONTH = new Array(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
955
+ Date.SECOND = 1000 /* milliseconds */
956
+ Date.MINUTE = 60 * Date.SECOND
957
+ Date.HOUR = 60 * Date.MINUTE
958
+ Date.DAY = 24 * Date.HOUR
959
+ Date.WEEK = 7 * Date.DAY
960
+
961
+ // Parses Date
962
+ Date.parseDate = function(str, fmt) {
963
+ if (str){
964
+ str = new String(str);
965
+ }else{
966
+ str = new String('');
967
+ }
968
+ str = str.strip();
969
+
970
+ var today = new Date();
971
+ var y = 0;
972
+ var m = -1;
973
+ var d = 0;
974
+ var a = str.split(/\W+/);
975
+ var b = fmt.match(/%./g);
976
+ var i = 0, j = 0;
977
+ var hr = 0;
978
+ var min = 0;
979
+
980
+ for (i = 0; i < a.length; ++i) {
981
+ if (!a[i]) continue;
982
+ switch (b[i]) {
983
+ case "%d":
984
+ case "%e":
985
+ d = parseInt(a[i], 10);
986
+ break;
987
+ case "%m":
988
+ m = parseInt(a[i], 10) - 1;
989
+ break;
990
+ case "%Y":
991
+ case "%y":
992
+ y = parseInt(a[i], 10);
993
+ (y < 100) && (y += (y > 29) ? 1900 : 2000);
994
+ break;
995
+ case "%b":
996
+ case "%B":
997
+ for (j = 0; j < 12; ++j) {
998
+ if (Calendar.MONTH_NAMES[j].substr(0, a[i].length).toLowerCase() == a[i].toLowerCase()) {
999
+ m = j;
1000
+ break;
1001
+ }
1002
+ }
1003
+ break;
1004
+ case "%H":
1005
+ case "%I":
1006
+ case "%k":
1007
+ case "%l":
1008
+ hr = parseInt(a[i], 10);
1009
+ break;
1010
+ case "%P":
1011
+ case "%p":
1012
+ if (/pm/i.test(a[i]) && hr < 12)
1013
+ hr += 12;
1014
+ else if (/am/i.test(a[i]) && hr >= 12)
1015
+ hr -= 12;
1016
+ break;
1017
+ case "%M":
1018
+ min = parseInt(a[i], 10);
1019
+ break;
1020
+ }
1021
+ }
1022
+ if (isNaN(y)) y = today.getFullYear();
1023
+ if (isNaN(m)) m = today.getMonth();
1024
+ if (isNaN(d)) d = today.getDate();
1025
+ if (isNaN(hr)) hr = today.getHours();
1026
+ if (isNaN(min)) min = today.getMinutes();
1027
+ if (y != 0 && m != -1 && d != 0)
1028
+ return new Date(y, m, d, hr, min, 0);
1029
+ y = 0; m = -1; d = 0;
1030
+ for (i = 0; i < a.length; ++i) {
1031
+ if (a[i].search(/[a-zA-Z]+/) != -1) {
1032
+ var t = -1;
1033
+ for (j = 0; j < 12; ++j) {
1034
+ if (Calendar.MONTH_NAMES[j].substr(0, a[i].length).toLowerCase() == a[i].toLowerCase()) { t = j; break; }
1035
+ }
1036
+ if (t != -1) {
1037
+ if (m != -1) {
1038
+ d = m+1;
1039
+ }
1040
+ m = t;
1041
+ }
1042
+ } else if (parseInt(a[i], 10) <= 12 && m == -1) {
1043
+ m = a[i]-1;
1044
+ } else if (parseInt(a[i], 10) > 31 && y == 0) {
1045
+ y = parseInt(a[i], 10);
1046
+ (y < 100) && (y += (y > 29) ? 1900 : 2000);
1047
+ } else if (d == 0) {
1048
+ d = a[i];
1049
+ }
1050
+ }
1051
+ if (y == 0)
1052
+ y = today.getFullYear();
1053
+ if (m != -1 && d != 0)
1054
+ return new Date(y, m, d, hr, min, 0);
1055
+ return today;
1056
+ };
1057
+
1058
+ // Returns the number of days in the current month
1059
+ Date.prototype.getMonthDays = function(month) {
1060
+ var year = this.getFullYear()
1061
+ if (typeof month == "undefined")
1062
+ month = this.getMonth()
1063
+ if (((0 == (year % 4)) && ( (0 != (year % 100)) || (0 == (year % 400)))) && month == 1)
1064
+ return 29
1065
+ else
1066
+ return Date.DAYS_IN_MONTH[month]
1067
+ };
1068
+
1069
+ // Returns the number of day in the year
1070
+ Date.prototype.getDayOfYear = function() {
1071
+ var now = new Date(this.getFullYear(), this.getMonth(), this.getDate(), 0, 0, 0);
1072
+ var then = new Date(this.getFullYear(), 0, 0, 0, 0, 0);
1073
+ var time = now - then;
1074
+ return Math.floor(time / Date.DAY);
1075
+ };
1076
+
1077
+ /** Returns the number of the week in year, as defined in ISO 8601. */
1078
+ Date.prototype.getWeekNumber = function() {
1079
+ var d = new Date(this.getFullYear(), this.getMonth(), this.getDate(), 0, 0, 0);
1080
+ var DoW = d.getDay();
1081
+ d.setDate(d.getDate() - (DoW + 6) % 7 + 3); // Nearest Thu
1082
+ var ms = d.valueOf(); // GMT
1083
+ d.setMonth(0);
1084
+ d.setDate(4); // Thu in Week 1
1085
+ return Math.round((ms - d.valueOf()) / (7 * 864e5)) + 1;
1086
+ };
1087
+
1088
+ /** Checks date and time equality */
1089
+ Date.prototype.equalsTo = function(date) {
1090
+ return ((this.getFullYear() == date.getFullYear()) &&
1091
+ (this.getMonth() == date.getMonth()) &&
1092
+ (this.getDate() == date.getDate()) &&
1093
+ (this.getHours() == date.getHours()) &&
1094
+ (this.getMinutes() == date.getMinutes()));
1095
+ };
1096
+
1097
+ /** Set only the year, month, date parts (keep existing time) */
1098
+ Date.prototype.setDateOnly = function(date) {
1099
+ var tmp = new Date(date);
1100
+ this.setDate(1);
1101
+ this.__setFullYear(tmp.getFullYear());
1102
+ this.setMonth(tmp.getMonth());
1103
+ this.setDate(tmp.getDate());
1104
+ };
1105
+
1106
+ /** Prints the date in a string according to the given format. */
1107
+ Date.prototype.print = function (str) {
1108
+ var m = this.getMonth();
1109
+ var d = this.getDate();
1110
+ var y = this.getFullYear();
1111
+ var wn = this.getWeekNumber();
1112
+ var w = this.getDay();
1113
+ var s = {};
1114
+ var hr = this.getHours();
1115
+ var pm = (hr >= 12);
1116
+ var ir = (pm) ? (hr - 12) : hr;
1117
+ var dy = this.getDayOfYear();
1118
+ if (ir == 0)
1119
+ ir = 12;
1120
+ var min = this.getMinutes();
1121
+ var sec = this.getSeconds();
1122
+ s["%a"] = Calendar.SHORT_DAY_NAMES[w]; // abbreviated weekday name [FIXME: I18N]
1123
+ s["%A"] = Calendar.DAY_NAMES[w]; // full weekday name
1124
+ s["%b"] = Calendar.SHORT_MONTH_NAMES[m]; // abbreviated month name [FIXME: I18N]
1125
+ s["%B"] = Calendar.MONTH_NAMES[m]; // full month name
1126
+ // FIXME: %c : preferred date and time representation for the current locale
1127
+ s["%C"] = 1 + Math.floor(y / 100); // the century number
1128
+ s["%d"] = (d < 10) ? ("0" + d) : d; // the day of the month (range 01 to 31)
1129
+ s["%e"] = d; // the day of the month (range 1 to 31)
1130
+ // FIXME: %D : american date style: %m/%d/%y
1131
+ // FIXME: %E, %F, %G, %g, %h (man strftime)
1132
+ s["%H"] = (hr < 10) ? ("0" + hr) : hr; // hour, range 00 to 23 (24h format)
1133
+ s["%I"] = (ir < 10) ? ("0" + ir) : ir; // hour, range 01 to 12 (12h format)
1134
+ s["%j"] = (dy < 100) ? ((dy < 10) ? ("00" + dy) : ("0" + dy)) : dy; // day of the year (range 001 to 366)
1135
+ s["%k"] = hr; // hour, range 0 to 23 (24h format)
1136
+ s["%l"] = ir; // hour, range 1 to 12 (12h format)
1137
+ s["%m"] = (m < 9) ? ("0" + (1+m)) : (1+m); // month, range 01 to 12
1138
+ s["%M"] = (min < 10) ? ("0" + min) : min; // minute, range 00 to 59
1139
+ s["%n"] = "\n"; // a newline character
1140
+ s["%p"] = pm ? "PM" : "AM";
1141
+ s["%P"] = pm ? "pm" : "am";
1142
+ // FIXME: %r : the time in am/pm notation %I:%M:%S %p
1143
+ // FIXME: %R : the time in 24-hour notation %H:%M
1144
+ s["%s"] = Math.floor(this.getTime() / 1000);
1145
+ s["%S"] = (sec < 10) ? ("0" + sec) : sec; // seconds, range 00 to 59
1146
+ s["%t"] = "\t"; // a tab character
1147
+ // FIXME: %T : the time in 24-hour notation (%H:%M:%S)
1148
+ s["%U"] = s["%W"] = s["%V"] = (wn < 10) ? ("0" + wn) : wn;
1149
+ s["%u"] = w + 1; // the day of the week (range 1 to 7, 1 = MON)
1150
+ s["%w"] = w; // the day of the week (range 0 to 6, 0 = SUN)
1151
+ // FIXME: %x : preferred date representation for the current locale without the time
1152
+ // FIXME: %X : preferred time representation for the current locale without the date
1153
+ s["%y"] = ('' + y).substr(2, 2); // year without the century (range 00 to 99)
1154
+ s["%Y"] = y; // year with the century
1155
+ s["%%"] = "%"; // a literal '%' character
1156
+
1157
+ return str.gsub(/%./, function(match) { return s[match] || match });
1158
+ };
1159
+
1160
+
1161
+ Date.prototype.__setFullYear = function(y) {
1162
+ var d = new Date(this);
1163
+ d.setFullYear(y);
1164
+ if (d.getMonth() != this.getMonth())
1165
+ this.setDate(28);
1166
+ this.setFullYear(y);
1167
+ };
1168
+