recurring_select 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ Copyright 2013 Jobber (OctopusApp Inc)
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
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,83 @@
1
+ RecurringSelect
2
+ =============
3
+
4
+ This is a gem to add a number of selectors and helpers for working with recurring schedules in a rails app.
5
+ It uses the [IceCube](https://github.com/seejohnrun/ice_cube) recurring scheduling gem.
6
+
7
+ Created by the [Jobber](http://getjobber.com) team for Jobber, the leading business management tool for field service companies.
8
+
9
+ Check out the [live demo](http://recurring-select-demo.herokuapp.com/)
10
+
11
+
12
+ Usage
13
+ -----
14
+
15
+ Basic selector:
16
+
17
+ Load the gem:
18
+ `gem 'recurring_select`
19
+
20
+ Require assets
21
+ Desktop view
22
+ application.js
23
+ `//= require recurring_select`
24
+ application.css
25
+ `//= require recurring_select`
26
+
27
+ or jQueryMobile interface
28
+ application.js
29
+ `//= require jquery-mobile-rs`
30
+ application.css
31
+ `//= require jquery-mobile-rs`
32
+
33
+
34
+ In the form view call the helper:
35
+ `<%= f.select_recurring :recurring_rule_column %>`
36
+
37
+ Options
38
+ -------
39
+
40
+ Defaults Values
41
+ ```
42
+ f.select_recurring :current_existing_rule, [
43
+ IceCube::Rule.weekly.day(:monday, :wednesday, :friday),
44
+ IceCube::Rule.monthly.day_of_month(-1)
45
+ ]
46
+ ```
47
+
48
+ :allow_blank let's you pick if there is a "not recurring" value
49
+ ```
50
+ f.select_recurring :current_existing_rule, :allow_blank => true
51
+ ```
52
+
53
+
54
+ Additional Helpers
55
+ ------------------
56
+
57
+ RecurringSelect also comes with several helpers for parsing up the
58
+ parameters when they hit your application.
59
+
60
+ You can send the column into the `is_valid_rule?` method to check the
61
+ validity of the input.
62
+ `RecurringSelect.is_valid_rule?(possible_rule)`
63
+
64
+ There is also a `dirty_hash_to_rule` method for sanitizing the inputs
65
+ for IceCube. This is sometimes needed based on if you're receiving strings, fixed
66
+ numbers, strings vs symbols, etc.
67
+ `RecurringSelect.dirty_hash_to_rule(params)`
68
+
69
+
70
+ Testing and development
71
+ ----------------------
72
+
73
+ Start the dummy server for clicking around the interface:
74
+ `rails s`
75
+
76
+ Use [Guard](https://github.com/guard/guard) and RSpec for all tests. I'd
77
+ love to get jasmine running also, but haven't had time yet.
78
+
79
+ Feel free to open issues or send pull requests.
80
+
81
+ Licensing
82
+ ---------
83
+ This project rocks and uses MIT-LICENSE.
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'RecurringSelect'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.rdoc')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+ APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
24
+ load 'rails/tasks/engine.rake'
25
+
26
+
27
+ Bundler::GemHelper.install_tasks
28
+
29
+ require 'rake/testtask'
30
+
31
+ Rake::TestTask.new(:test) do |t|
32
+ t.libs << 'lib'
33
+ t.libs << 'test'
34
+ t.pattern = 'test/**/*_test.rb'
35
+ t.verbose = false
36
+ end
37
+
38
+
39
+ task :default => :test
@@ -0,0 +1,15 @@
1
+ //= require recurring_select
2
+ //= require_self
3
+
4
+ $ ->
5
+ $(document).on "recurring_select:cancel recurring_select:save", ".recurring_select", ->
6
+ $(this).selectmenu('refresh')
7
+
8
+ $(document).on "recurring_select:dialog_opened", ".rs_dialog_holder", ->
9
+ $(this).find("select").attr("data-theme", $('.recurring_select').data("theme")).attr("data-mini", true).selectmenu()
10
+ $(this).find("input[type=text]").textinput()
11
+
12
+ $(this).on "recurring_select:dialog_positioned", ".rs_dialog", ->
13
+ $(this).css
14
+ "top" : $(window).scrollTop()+"px"
15
+
@@ -0,0 +1,74 @@
1
+ //= require recurring_select_dialog
2
+ //= require_self
3
+
4
+ $ = jQuery
5
+ $ ->
6
+ $(document).on "focus", ".recurring_select", ->
7
+ $(this).recurring_select('set_initial_values')
8
+
9
+ $(document).on "change", ".recurring_select", ->
10
+ $(this).recurring_select('changed')
11
+
12
+ methods =
13
+ set_initial_values: ->
14
+ @data 'initial-value-hash', @val()
15
+ @data 'initial-value-str', $(@find("option").get()[@.prop("selectedIndex")]).text()
16
+
17
+ changed: ->
18
+ if @val() == "custom"
19
+ methods.open_custom.apply(@)
20
+ else
21
+ methods.set_initial_values.apply(@)
22
+
23
+ open_custom: ->
24
+ @data "recurring-select-active", true
25
+ new RecurringSelectDialog(@)
26
+ @blur()
27
+
28
+ save: (new_rule) ->
29
+ @find("option[data-custom]").remove()
30
+ new_json_val = JSON.stringify(new_rule.hash)
31
+
32
+ # TODO: check for matching name, and replace that value if found
33
+
34
+ if $.inArray(new_json_val, @find("option").map -> $(@).val()) == -1
35
+ methods.insert_option.apply @, [new_rule.str, new_json_val]
36
+
37
+ @val new_json_val
38
+ methods.set_initial_values.apply @
39
+ @.trigger "recurring_select:save"
40
+
41
+ current_rule: ->
42
+ str: @data("initial-value-str")
43
+ hash: $.parseJSON(@data("initial-value-hash"))
44
+
45
+ cancel: ->
46
+ @val @data("initial-value-hash")
47
+ @data "recurring-select-active", false
48
+ @.trigger "recurring_select:cancel"
49
+
50
+
51
+ insert_option: (new_rule_str, new_rule_json) ->
52
+ seperator = @find("option:disabled")
53
+ if seperator.length == 0
54
+ seperator = @find("option")
55
+ seperator = seperator.last()
56
+
57
+ new_option = $(document.createElement("option"))
58
+ new_option.attr "data-custom", true
59
+
60
+ if new_rule_str.substr(new_rule_str.length - 1) != "*"
61
+ new_rule_str+="*"
62
+
63
+ new_option.text new_rule_str
64
+ new_option.val new_rule_json
65
+ new_option.insertBefore seperator
66
+
67
+ methods: ->
68
+ methods
69
+
70
+ $.fn.recurring_select = (method) ->
71
+ if method of methods
72
+ return methods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 ) );
73
+ else
74
+ $.error( "Method #{method} does not exist on jQuery.recurring_select" );
@@ -0,0 +1,300 @@
1
+ <%
2
+ template_path = File.expand_path("../templates/recurring_select/dialog_template.html", File.dirname(__FILE__))
3
+ File.open(template_path) do |file|
4
+ @template = file.read
5
+ end
6
+ %>
7
+
8
+ window.RecurringSelectDialog =
9
+ class RecurringSelectDialog
10
+ constructor: (@recurring_selector) ->
11
+ @current_rule = @recurring_selector.recurring_select('current_rule')
12
+ @initDialogBox()
13
+ if not @current_rule.hash? or not @current_rule.hash.rule_type?
14
+ @freqChanged()
15
+ else
16
+ setTimeout @positionDialogVert, 10 # allow initial render
17
+
18
+ initDialogBox: ->
19
+ $(".rs_dialog_holder").remove()
20
+
21
+ open_in = $("body")
22
+ open_in = $(".ui-page-active") if $(".ui-page-active").length
23
+ open_in.append "<%= ActionController::Base.helpers.escape_javascript @template %>"
24
+ @outer_holder = $(".rs_dialog_holder")
25
+ @inner_holder = @outer_holder.find ".rs_dialog"
26
+ @content = @outer_holder.find ".rs_dialog_content"
27
+
28
+ @positionDialogVert(true)
29
+ @mainEventInit()
30
+ @freqInit()
31
+ @summaryInit()
32
+ @freq_select.focus()
33
+ @outer_holder.trigger "recurring_select:dialog_opened"
34
+
35
+ positionDialogVert: (initial_positioning) =>
36
+ window_height = $(window).height()
37
+ window_width = $(window).width()
38
+ dialog_height = @content.outerHeight()
39
+ if dialog_height < 80
40
+ dialog_height = 80
41
+ margin_top = (window_height - dialog_height)/2 - 30
42
+ margin_top = 10 if margin_top < 10
43
+ # if dialog_height > window_height - 20
44
+ # dialog_height = window_height - 20
45
+
46
+ new_style_hash =
47
+ "margin-top" : margin_top+"px"
48
+ "height" : dialog_height+"px"
49
+
50
+ if initial_positioning?
51
+ @inner_holder.css new_style_hash
52
+ @inner_holder.trigger "recurring_select:dialog_positioned"
53
+ else
54
+ @content.css {"width": @content.width()+"px"}
55
+ @inner_holder.addClass "animated"
56
+ @inner_holder.animate new_style_hash, 200, =>
57
+ @inner_holder.removeClass "animated"
58
+ @content.css {"width": "auto"}
59
+ @inner_holder.trigger "recurring_select:dialog_positioned"
60
+
61
+ cancel: =>
62
+ @outer_holder.remove()
63
+ @recurring_selector.recurring_select('cancel')
64
+
65
+ outerCancel: (event) =>
66
+ if $(event.target).hasClass("rs_dialog_holder")
67
+ @cancel()
68
+
69
+ save: =>
70
+ return if !@current_rule.str?
71
+ @outer_holder.remove()
72
+ @recurring_selector.recurring_select('save', @current_rule)
73
+
74
+ # ========================= Init Methods ===============================
75
+
76
+ mainEventInit: ->
77
+ # Tap hooks are for jQueryMobile
78
+ @outer_holder.on 'click tap', @outerCancel
79
+ @content.on 'click tap', 'h1 a', @cancel
80
+ @save_button = @content.find('input.rs_save').on "click tap", @save
81
+ @content.find('input.rs_cancel').on "click tap", @cancel
82
+
83
+ freqInit: ->
84
+ @freq_select = @outer_holder.find ".rs_frequency"
85
+ if @current_rule.hash? && (rule_type = @current_rule.hash.rule_type)?
86
+ if rule_type.search(/Weekly/) != -1
87
+ @freq_select.prop('selectedIndex', 1)
88
+ @initWeeklyOptions()
89
+ else if rule_type.search(/Monthly/) != -1
90
+ @freq_select.prop('selectedIndex', 2)
91
+ @initMonthlyOptions()
92
+ else if rule_type.search(/Yearly/) != -1
93
+ @freq_select.prop('selectedIndex', 3)
94
+ @initYearlyOptions()
95
+ else
96
+ @initDailyOptions()
97
+ @freq_select.on "change", @freqChanged
98
+
99
+ initDailyOptions: ->
100
+ section = @content.find('.daily_options')
101
+ interval_input = section.find('.rs_daily_interval')
102
+ interval_input.val(@current_rule.hash.interval)
103
+ interval_input.on "change keyup", @intervalChanged
104
+ section.show()
105
+
106
+ initWeeklyOptions: ->
107
+ section = @content.find('.weekly_options')
108
+
109
+ # connect the interval field
110
+ interval_input = section.find('.rs_weekly_interval')
111
+ interval_input.val(@current_rule.hash.interval)
112
+ interval_input.on "change keyup", @intervalChanged
113
+
114
+ # connect the day fields
115
+ if @current_rule.hash.validations? && @current_rule.hash.validations.day?
116
+ $(@current_rule.hash.validations.day).each (index, val) ->
117
+ section.find(".day_holder a[data-value='"+val+"']").addClass("selected")
118
+ section.on "click", ".day_holder a", @daysChanged
119
+
120
+ section.show()
121
+
122
+ initMonthlyOptions: ->
123
+ section = @content.find('.monthly_options')
124
+ interval_input = section.find('.rs_monthly_interval')
125
+ interval_input.val(@current_rule.hash.interval)
126
+ interval_input.on "change keyup", @intervalChanged
127
+
128
+ @current_rule.hash.validations ||= {}
129
+ @current_rule.hash.validations.day_of_month ||= []
130
+ @current_rule.hash.validations.day_of_week ||= {}
131
+ @init_calendar_days(section)
132
+ @init_calendar_weeks(section)
133
+
134
+ in_week_mode = Object.keys(@current_rule.hash.validations.day_of_week).length > 0
135
+ section.find(".monthly_rule_type_week").prop("checked", in_week_mode)
136
+ section.find(".monthly_rule_type_day").prop("checked", !in_week_mode)
137
+ @toggle_month_view()
138
+ section.find("input[name=monthly_rule_type]").on "change", @toggle_month_view
139
+ section.show()
140
+
141
+ initYearlyOptions: ->
142
+ section = @content.find('.yearly_options')
143
+ interval_input = section.find('.rs_yearly_interval')
144
+ interval_input.val(@current_rule.hash.interval)
145
+ interval_input.on "change keyup", @intervalChanged
146
+ section.show()
147
+
148
+
149
+ summaryInit: ->
150
+ @summary = @outer_holder.find(".rs_summary")
151
+ @summaryUpdate()
152
+
153
+ # ========================= render methods ===============================
154
+
155
+ summaryUpdate: (new_string) =>
156
+ if @current_rule.hash? && @current_rule.str?
157
+ @summary.removeClass "fetching"
158
+ @save_button.removeClass("disabled")
159
+ rule_str = @current_rule.str.replace("*", "")
160
+ if rule_str.length < 20
161
+ rule_str = "Summary: "+rule_str
162
+ @summary.find("span").html rule_str
163
+ else
164
+ @summary.addClass "fetching"
165
+ @save_button.addClass("disabled")
166
+ @summary.find("span").html ""
167
+ @summaryFetch()
168
+
169
+ summaryFetch: ->
170
+ return if !(@current_rule.hash? && (rule_type = @current_rule.hash.rule_type)?)
171
+ @content.css {"width": @content.width()+"px"}
172
+ $.ajax
173
+ url: "/recurring_select/translate",
174
+ type: "POST",
175
+ data: @current_rule.hash
176
+ success: @summaryFetchSuccess
177
+
178
+ summaryFetchSuccess: (data) =>
179
+ @current_rule.str = data
180
+ @summaryUpdate()
181
+ @content.css {"width": "auto"}
182
+
183
+ init_calendar_days: (section) =>
184
+ monthly_calendar = section.find(".rs_calendar_day")
185
+ monthly_calendar.html ""
186
+ for num in [1..31]
187
+ monthly_calendar.append (day_link = $(document.createElement("a")).text(num))
188
+ if $.inArray(num, @current_rule.hash.validations.day_of_month) != -1
189
+ day_link.addClass("selected")
190
+
191
+ # add last day of month button
192
+ monthly_calendar.append (end_of_month_link = $(document.createElement("a")).text("Last Day"))
193
+ end_of_month_link.addClass("end_of_month")
194
+ if $.inArray(-1, @current_rule.hash.validations.day_of_month) != -1
195
+ end_of_month_link.addClass("selected")
196
+
197
+ monthly_calendar.find("a").on "click tap", @dateOfMonthChanged
198
+
199
+ init_calendar_weeks: (section) =>
200
+ monthly_calendar = section.find(".rs_calendar_week")
201
+ monthly_calendar.html ""
202
+ row_labels = ["1st", "2nd", "3rd", "4th"]
203
+ cell_str = ["S", "M", "T", "W", "T", "F", "S"]
204
+
205
+ for num in [1..4]
206
+ monthly_calendar.append "<span>#{row_labels[num - 1]}</span>"
207
+ for day_of_week in [0..6]
208
+ day_link = $("<a>", {text: cell_str[day_of_week]})
209
+ day_link.attr("day", day_of_week)
210
+ day_link.attr("instance", num)
211
+ monthly_calendar.append day_link
212
+ $.each @current_rule.hash.validations.day_of_week, (key, value) ->
213
+ $.each value, (index, instance) ->
214
+ section.find("a[day='#{key}'][instance='#{instance}']").addClass("selected")
215
+ monthly_calendar.find("a").on "click tap", @weekOfMonthChanged
216
+
217
+ toggle_month_view: =>
218
+ week_mode = @content.find(".monthly_rule_type_week").prop("checked")
219
+ @content.find(".rs_calendar_week").toggle(week_mode)
220
+ @content.find(".rs_calendar_day").toggle(!week_mode)
221
+
222
+ # ========================= Change callbacks ===============================
223
+
224
+ freqChanged: =>
225
+ @current_rule.hash = null unless $.isPlainObject(@current_rule.hash) # for custom values
226
+
227
+ @current_rule.hash ||= {}
228
+ @current_rule.hash.interval = 1
229
+ @current_rule.hash.until = null
230
+ @current_rule.hash.count = null
231
+ @current_rule.hash.validations = null
232
+ @content.find(".freq_option_section").hide();
233
+ @content.find("input[type=radio], input[type=checkbox]").prop("checked", false)
234
+ switch @freq_select.val()
235
+ when "Weekly"
236
+ @current_rule.hash.rule_type = "IceCube::WeeklyRule"
237
+ @current_rule.str = "Weekly"
238
+ @initWeeklyOptions()
239
+ when "Monthly"
240
+ @current_rule.hash.rule_type = "IceCube::MonthlyRule"
241
+ @current_rule.str = "Monthly"
242
+ @initMonthlyOptions()
243
+ when "Yearly"
244
+ @current_rule.hash.rule_type = "IceCube::YearlyRule"
245
+ @current_rule.str = "Yearly"
246
+ @initYearlyOptions()
247
+ else
248
+ @current_rule.hash.rule_type = "IceCube::DailyRule"
249
+ @current_rule.str = "Daily"
250
+ @initDailyOptions()
251
+ @summaryUpdate()
252
+ @positionDialogVert()
253
+
254
+ intervalChanged: (event) =>
255
+ @current_rule.str = null
256
+ @current_rule.hash ||= {}
257
+ @current_rule.hash.interval = parseInt($(event.currentTarget).val())
258
+ if @current_rule.hash.interval < 1 || isNaN(@current_rule.hash.interval)
259
+ @current_rule.hash.interval = 1
260
+ # $(event.currentTarget).val(@current_rule.hash.interval)
261
+ @summaryUpdate()
262
+
263
+ daysChanged: (event) =>
264
+ $(event.currentTarget).toggleClass("selected")
265
+ @current_rule.str = null
266
+ @current_rule.hash ||= {}
267
+ @current_rule.hash.validations = {}
268
+ raw_days = @content.find(".day_holder a.selected").map -> parseInt($(this).data("value"))
269
+ @current_rule.hash.validations.day = raw_days.get()
270
+ @summaryUpdate()
271
+ false # this prevents default and propogation
272
+
273
+ dateOfMonthChanged: (event) =>
274
+ $(event.currentTarget).toggleClass("selected")
275
+ @current_rule.str = null
276
+ @current_rule.hash ||= {}
277
+ @current_rule.hash.validations = {}
278
+ raw_days = @content.find(".monthly_options .rs_calendar_day a.selected").map ->
279
+ res = if $(this).text() == "Last Day" then -1 else parseInt($(this).text())
280
+ res
281
+ @current_rule.hash.validations.day_of_week = {}
282
+ @current_rule.hash.validations.day_of_month = raw_days.get()
283
+ @summaryUpdate()
284
+ false
285
+
286
+ weekOfMonthChanged: (event) =>
287
+ $(event.currentTarget).toggleClass("selected")
288
+ @current_rule.str = null
289
+ @current_rule.hash ||= {}
290
+ @current_rule.hash.validations = {}
291
+ @current_rule.hash.validations.day_of_month = []
292
+ @current_rule.hash.validations.day_of_week = {}
293
+ @content.find(".monthly_options .rs_calendar_week a.selected").each (index, elm) =>
294
+ day = parseInt($(elm).attr("day"))
295
+ instance = parseInt($(elm).attr("instance"))
296
+ @current_rule.hash.validations.day_of_week[day] ||= []
297
+ @current_rule.hash.validations.day_of_week[day].push instance
298
+ @summaryUpdate()
299
+ false
300
+
@@ -0,0 +1,33 @@
1
+ /*
2
+ *= require recurring_select
3
+ *= require_self
4
+ */
5
+
6
+ .rs_dialog_holder {padding-left:0px; background-color:#333; background-color:rgba(30,30,30,0.3); font-size:1em; position:absolute;
7
+ .rs_dialog { position:absolute; left:10px; right:10px; margin:0px; display:block; z-index:51;
8
+
9
+
10
+ .rs_dialog_content {
11
+
12
+ label.ui-select { display:inline-block;}
13
+ div.ui-select {display:inline-block;}
14
+
15
+ input.rs_interval {display:inline-block;}
16
+
17
+ .freq_option_section {
18
+ .day_holder {height:36px;
19
+ a {padding:10px 12px;}
20
+ }
21
+
22
+ .rs_calendar { width:225px;
23
+ * { text-shadow: none; }
24
+ a {padding:10px 8px;}
25
+ a.end_of_month { width: 111px; }
26
+ }
27
+ }
28
+
29
+ .rs_summary span { font-size:90%; font-weight:normal; }
30
+ }
31
+
32
+ }
33
+ }
@@ -0,0 +1,101 @@
1
+ @import "utilities.scss";
2
+
3
+ /* -------- resets ---------------*/
4
+
5
+ .rs_dialog_holder { font-size:14px; color:black;
6
+ a {color:black;}
7
+ input[type=button] {
8
+ font: small/normal Arial,sans-serif;
9
+ background: #F5F5F5; color: #444; border: 1px solid #ccc;
10
+ font-size: 11px; font-weight: bold; height: 27px; line-height: 27px; outline: none; padding: 0 8px; text-align: center;
11
+
12
+ @include rounded_corners(2px);
13
+ @include gradiant(#f5f5f5, #f1f1f1);
14
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorStr='#f5f5f5',EndColorStr='#f1f1f1');
15
+
16
+ &:hover {border-color:#aaa; color:#222; background-color:#f9f9f9; @include shadows(0px, 1px, 2px, rgba(0,0,0,0.2)); }
17
+ &:focus {border-color:#1E90FF;}
18
+ &:active {border-color:#1E90FF;}
19
+ }
20
+ }
21
+
22
+ /*------- defaults ------------ */
23
+ .rs_dialog_holder { font-family:helvetica, arial, 'san-serif'; color:#222; font-size:12px;}
24
+
25
+ /*------- specifics ------------ */
26
+
27
+ select {
28
+ option.bold {font-weight:bold; color:red;}
29
+ }
30
+
31
+ .rs_dialog_holder { position:fixed; left:0px; right:0px; top:0px; bottom:0px; padding-left:50%; background-color:rgba(255,255,255,0.2); z-index:50;
32
+ .rs_dialog { background-color:#f6f6f6; border:1px solid #acacac; @include shadows(1px, 3px, 8px, rgba(0,0,0,0.25)); @include rounded_corners(7px);
33
+ display:inline-block; min-width:200px; margin-left:-125px; overflow:hidden; height:25px; position:relative;
34
+ .rs_dialog_content { padding:10px;
35
+ h1 { font-size:16px; padding:0px; margin:0 0 10px 0;
36
+ a {float:right; display:inline-block; height:16px; width:16px; background-image:url(<%=asset_path "recurring_select/cancel.png"%>); background-position:center; background-repeat:no-repeat;}
37
+ }
38
+
39
+ p { padding:5px 0; margin:0;
40
+ label {margin-right:10px;}
41
+ }
42
+
43
+ .freq_option_section { display:none;
44
+ label { font-weight: bold; }
45
+ .rs_interval {width:30px; text-align:center;}
46
+
47
+ .day_holder { border-left:1px solid #ccc; position:relative; margin-top:5px; height:26px;
48
+ a {display:block; padding:5px 7px; font-size:14px; border-style:solid; border-color:#ccc; border-width:1px 1px 1px 0px; float:left; text-decoration:none; font-weight:bold; @include inset_shadows(0px, 1px, 2px, rgba(0,0,0,0.1)); background-color:#fff;;
49
+ &.selected {background-color:#89a; color:#fff; @include inset_shadows(1px, 1px, 2px, rgba(0,0,0,0.2)); @include gradiant(#9ab, #789); }
50
+ &:hover { cursor:pointer; background-color:#dde;}
51
+ }
52
+ }
53
+
54
+ .rs_calendar_day, .rs_calendar_week {
55
+ width:155px;
56
+ a {display:inline-block; text-align:center; width:15px; padding:5px 3px; font-size:12px; border-style:solid; border-color:#ccc; border-width:1px 1px 1px 1px; margin:-1px 0 0 -1px; line-height:10px; background-color:#fff; font-weight:bold;
57
+ &.selected {background-color:#89a; color:#fff; @include gradiant(#9ab, #789); @include inset_shadows(0px, 1px, 2px, rgba(0,0,0,0.2)); text-shadow:0 1px 1px #333;}
58
+ &:hover { cursor:pointer; background-color:#dde;}
59
+ }
60
+ a.end_of_month { width: 81px; }
61
+ }
62
+ .rs_calendar_week {
63
+ width: 183px;
64
+ span {
65
+ display: inline-block;
66
+ width: 27px;
67
+ }
68
+ }
69
+
70
+ .monthly_rule_type {
71
+ span {
72
+ margin-right: 15px;
73
+ }
74
+ }
75
+ }
76
+
77
+
78
+ .rs_summary { padding:0px; margin-top:15px; border-top:1px solid #ccc;
79
+ span {font-weight:bold; border-top:1px solid #fff; display:block; padding:10px 0 5px 0;}
80
+ &.fetching {color:#999;
81
+ span {background-image:url(<%=asset_path "recurring_select/throbber_13x13.gif" %>); background-position:center; background-repeat:no-repeat; display:inline-block; height:13px; width:13px; margin-top:-4px; padding-right:5px;}
82
+ }
83
+ label {font-weight:normal;}
84
+ }
85
+
86
+ .controls { padding:10px 0px 5px 0px; min-width:170px; text-align:center;
87
+ input[type=button] { margin:0px 5px; width:70px;
88
+ &.rs_save {color:#333; }
89
+ &.rs_cancel {color:#666;}
90
+ &.disabled {color:#aaa; }
91
+ }
92
+ }
93
+ }
94
+
95
+ &.animated {
96
+ .controls {position:absolute; bottom:10px; left:10px;}
97
+ .rs_summary, .freq_option_section {display:none;}
98
+ }
99
+
100
+ }
101
+ }
@@ -0,0 +1,26 @@
1
+ @mixin shadows($x_offset: 0px, $y_offset: 1px, $size: 8px, $color: rgba(0, 0, 0, 0.5)) {
2
+ -webkit-box-shadow: $x_offset $y_offset $size $color;
3
+ -moz-box-shadow: $x_offset $y_offset $size $color;
4
+ -ms-box-shadow: $x_offset $y_offset $size $color;
5
+ -o-box-shadow: $x_offset $y_offset $size $color;
6
+ box-shadow: $x_offset $y_offset $size $color;
7
+ }
8
+
9
+ @mixin inset_shadows($x_offset: 0px, $y_offset: 1px, $size: 8px, $color: rgba(0, 0, 0, 0.5)) {
10
+ -webkit-box-shadow: $x_offset $y_offset $size $color inset;
11
+ -moz-box-shadow: $x_offset $y_offset $size $color inset;
12
+ -ms-box-shadow: $x_offset $y_offset $size $color inset;
13
+ -o-box-shadow: $x_offset $y_offset $size $color inset;
14
+ box-shadow: $x_offset $y_offset $size $color inset;
15
+ }
16
+
17
+ @mixin rounded_corners($radius: 5px) { -moz-border-radius: $radius; -webkit-border-radius: $radius; border-radius: $radius;}
18
+
19
+ @mixin gradiant ($color_a:#aaa, $color_b:#bbb) {
20
+ position:relative;
21
+ background-image: -webkit-linear-gradient(top, $color_a, $color_b); /* Chrome 10+, Saf5.1+ */
22
+ background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from($color_a), to($color_b)); /* Saf4+, Chrome */
23
+ background-image: -moz-linear-gradient(top,$color_a,$color_b); /* FF3.6 */
24
+ background-image: -ms-linear-gradient(top, $color_a, $color_b); /* IE10 */
25
+ background-image: -o-linear-gradient(top, $color_a, $color_b); /* Opera 11.10+ */
26
+ }
@@ -0,0 +1,77 @@
1
+ <div class='rs_dialog_holder'>
2
+ <div class='rs_dialog'>
3
+ <div class='rs_dialog_content'>
4
+ <h1>Repeat <a href='#' title='Cancel' Alt='Cancel'></a> </h1>
5
+
6
+ <p>
7
+ <label for='rs_frequency'>Frequency:</label>
8
+ <select id='rs_frequency' class='rs_frequency' name='rs_frequency'>
9
+ <option value="Daily">Daily</option>
10
+ <option value="Weekly">Weekly</option>
11
+ <option value="Monthly">Monthly</option>
12
+ <option value="Yearly">Yearly</option>
13
+ </select>
14
+ </p>
15
+
16
+ <div class="daily_options freq_option_section">
17
+ <p>
18
+ Every
19
+ <input type='text' name='rs_daily_interval' class='rs_daily_interval rs_interval' value='1' size='2' />
20
+ day(s)
21
+ </p>
22
+ </div>
23
+
24
+ <div class="weekly_options freq_option_section">
25
+ <p>
26
+ Every
27
+ <input type='text' name='rs_weekly_interval' class='rs_weekly_interval rs_interval' value='1' size='2' />
28
+ week(s) on :
29
+ </p>
30
+ <div class="day_holder">
31
+ <a href="#" data-value='0'>S</a>
32
+ <a href="#" data-value='1'>M</a>
33
+ <a href="#" data-value='2'>T</a>
34
+ <a href="#" data-value='3'>W</a>
35
+ <a href="#" data-value='4'>T</a>
36
+ <a href="#" data-value='5'>F</a>
37
+ <a href="#" data-value='6'>S</a>
38
+ </div>
39
+
40
+ <span style="clear:both; visibility:hidden; height:1px;">.</span>
41
+ </div>
42
+
43
+ <div class="monthly_options freq_option_section">
44
+ <p>
45
+ Every
46
+ <input type='text' name='rs_monthly_interval' class='rs_monthly_interval rs_interval' value='1' size='2' />
47
+ month(s):
48
+ </p>
49
+ <p class="monthly_rule_type">
50
+ <span>Day of Month <input type="radio" class="monthly_rule_type_day" name="monthly_rule_type" value="true" /></span>
51
+ <span>Day of Week <input type="radio" class="monthly_rule_type_week" name="monthly_rule_type" value="true" /></span>
52
+ </p>
53
+ <p class="rs_calendar_day"></p>
54
+ <p class="rs_calendar_week"></p>
55
+ </div>
56
+
57
+ <div class="yearly_options freq_option_section">
58
+ <p>
59
+ Every
60
+ <input type='text' name='rs_yearly_interval' class='rs_yearly_interval rs_interval' value='1' size='2' />
61
+ year(s)
62
+ </p>
63
+ </div>
64
+
65
+
66
+ <p class='rs_summary'>
67
+ <span></span>
68
+ </p>
69
+
70
+ <div class="controls">
71
+ <input type="button" class="rs_save" value='OK' />
72
+ <input type="button" class="rs_cancel" value='Cancel' />
73
+ </div>
74
+
75
+ </div>
76
+ </div>
77
+ </div>
@@ -0,0 +1,107 @@
1
+ require "ice_cube"
2
+
3
+ module RecurringSelectHelper
4
+ module FormHelper
5
+ def select_recurring(object, method, default_schedules = nil, options = {}, html_options = {})
6
+ InstanceTag.new(object, method, self, options.delete(:object)).to_recurring_select_tag(default_schedules, options, html_options)
7
+ end
8
+ end
9
+
10
+ module FormOptionsHelper
11
+ def recurring_options_for_select(currently_selected_rule = nil, default_schedules = nil, options = {})
12
+
13
+ options_array = []
14
+ blank_option_label = options[:blank_label] || "- not recurring -"
15
+ blank_option = [blank_option_label, "null"]
16
+ seperator = ["or", {:disabled => true}]
17
+
18
+ if default_schedules.blank?
19
+ if currently_selected_rule
20
+ options_array << blank_option if options[:allow_blank]
21
+ options_array << ice_cube_rule_to_option(currently_selected_rule)
22
+ options_array << seperator
23
+ options_array << ["Change schedule...", "custom"]
24
+ else
25
+ options_array << blank_option
26
+ options_array << ["Set schedule...", "custom"]
27
+ end
28
+ else
29
+ options_array << blank_option if options[:allow_blank]
30
+
31
+ options_array += default_schedules.collect{|dc|
32
+ ice_cube_rule_to_option(dc)
33
+ }
34
+
35
+ if currently_selected_rule and not current_rule_in_defaults?(currently_selected_rule, default_schedules)
36
+ options_array << ice_cube_rule_to_option(currently_selected_rule, true)
37
+ custom_label = ["New custom schedule...", "custom"]
38
+ else
39
+ custom_label = ["Custom schedule...", "custom"]
40
+ end
41
+
42
+ options_array << seperator
43
+ options_array << custom_label
44
+ end
45
+
46
+ options_for_select(options_array, currently_selected_rule.to_json)
47
+ end
48
+
49
+ private
50
+
51
+ def ice_cube_rule_to_option(supplied_rule, custom = false)
52
+ return supplied_rule unless RecurringSelect.is_valid_rule?(supplied_rule)
53
+
54
+ rule = supplied_rule.is_a?(Hash) ? IceCube::Rule.from_hash(supplied_rule) : supplied_rule
55
+ ar = [rule.to_s, rule.to_hash.to_json]
56
+
57
+ if custom
58
+ ar[0] << "*"
59
+ ar << {"data-custom" => true}
60
+ end
61
+
62
+ ar
63
+ end
64
+
65
+ def current_rule_in_defaults?(currently_selected_rule, default_schedules)
66
+ default_schedules.any?{|option|
67
+ option == currently_selected_rule or
68
+ (option.is_a?(Array) and option[1] == currently_selected_rule)
69
+ }
70
+ end
71
+
72
+ end
73
+
74
+ class InstanceTag < ActionView::Helpers::InstanceTag
75
+ include FormOptionsHelper
76
+
77
+ def to_recurring_select_tag(default_schedules, options, html_options)
78
+ html_options = recurring_select_html_options(html_options)
79
+ add_default_name_and_id(html_options)
80
+ value = value(object)
81
+ content_tag("select",
82
+ add_options(
83
+ recurring_options_for_select(value, default_schedules, options),
84
+ options, value
85
+ ), html_options
86
+ )
87
+ end
88
+
89
+ private
90
+
91
+ def recurring_select_html_options(html_options)
92
+ html_options = html_options.stringify_keys
93
+ html_options["class"] = ((html_options["class"] || "").split() + ["recurring_select"]).join(" ")
94
+ html_options
95
+ end
96
+ end
97
+
98
+ module FormBuilder
99
+ def select_recurring(method, default_schedules = nil, options = {}, html_options = {})
100
+ if !@template.respond_to?(:select_recurring)
101
+ @template.class.send(:include, RecurringSelectHelper::FormHelper)
102
+ end
103
+
104
+ @template.select_recurring(@object_name, method, default_schedules, options.merge(:object => @object), html_options)
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,26 @@
1
+ require "ice_cube"
2
+
3
+ class RecurringSelectMiddleware
4
+
5
+ def initialize(app)
6
+ @app = app
7
+ end
8
+
9
+ def call(env)
10
+ if env["PATH_INFO"] =~ /^\/recurring_select\/translate/
11
+ request = Rack::Request.new(env)
12
+ params = request.params
13
+ params.symbolize_keys!
14
+
15
+ if params and params[:rule_type]
16
+ rule = RecurringSelect.dirty_hash_to_rule(params)
17
+ [200, {"Content-Type" => "text/html"}, [rule.to_s]]
18
+ else
19
+ [200, {"Content-Type" => "text/html"}, [""]]
20
+ end
21
+ else
22
+ @app.call(env)
23
+ end
24
+ end
25
+
26
+ end
@@ -0,0 +1,2 @@
1
+ Rails.application.routes.draw do
2
+ end
@@ -0,0 +1,90 @@
1
+ require "recurring_select/engine"
2
+ require "ice_cube"
3
+
4
+ #TODO: remove monkey patch when https://github.com/seejohnrun/ice_cube/issues/136 is fixed
5
+ IceCube::ValidatedRule.class_eval do
6
+ alias old_to_s to_s
7
+ def to_s
8
+ old_to_s.gsub("when it is", "and")
9
+ end
10
+ end
11
+
12
+ module RecurringSelect
13
+
14
+ def self.dirty_hash_to_rule(params)
15
+ if params.is_a? IceCube::Rule
16
+ params
17
+ else
18
+ if params.is_a?(String)
19
+ params = JSON.parse(params)
20
+ end
21
+
22
+ params.symbolize_keys!
23
+ rules_hash = filter_params(params)
24
+
25
+ IceCube::Rule.from_hash(rules_hash)
26
+ end
27
+ end
28
+
29
+ def self.is_valid_rule?(possible_rule)
30
+ return true if possible_rule.is_a?(IceCube::Rule)
31
+ return false if possible_rule.blank?
32
+
33
+ if possible_rule.is_a?(String)
34
+ begin
35
+ JSON.parse(possible_rule)
36
+ return true
37
+ rescue JSON::ParserError
38
+ return false
39
+ end
40
+ end
41
+
42
+ # TODO: this should really have an extra step where it tries to perform the final parsing
43
+ return true if possible_rule.is_a?(Hash)
44
+
45
+ false #only a hash or a string of a hash can be valid
46
+ end
47
+
48
+ private
49
+
50
+ def self.filter_params(params)
51
+ params.reject!{|key, value| value.blank? || value=="null" }
52
+
53
+ params[:interval] = params[:interval].to_i if params[:interval]
54
+
55
+ params[:validations] ||= {}
56
+ params[:validations].symbolize_keys!
57
+
58
+ if params[:validations][:day]
59
+ params[:validations][:day] = params[:validations][:day].collect(&:to_i)
60
+ end
61
+
62
+ if params[:validations][:day_of_month]
63
+ params[:validations][:day_of_month] = params[:validations][:day_of_month].collect(&:to_i)
64
+ end
65
+
66
+ # this is soooooo ugly
67
+ if params[:validations][:day_of_week]
68
+ params[:validations][:day_of_week] ||= {}
69
+ if params[:validations][:day_of_week].length > 0 and not params[:validations][:day_of_week].keys.first =~ /\d/
70
+ params[:validations][:day_of_week].symbolize_keys!
71
+ else
72
+ originals = params[:validations][:day_of_week].dup
73
+ params[:validations][:day_of_week] = {}
74
+ originals.each{|key, value|
75
+ params[:validations][:day_of_week][key.to_i] = value
76
+ }
77
+ end
78
+ params[:validations][:day_of_week].each{|key, value|
79
+ params[:validations][:day_of_week][key] = value.collect(&:to_i)
80
+ }
81
+ end
82
+
83
+ if params[:validations][:day_of_year]
84
+ params[:validations][:day_of_year] = params[:validations][:day_of_year].collect(&:to_i)
85
+ end
86
+
87
+ params
88
+ end
89
+
90
+ end
@@ -0,0 +1,16 @@
1
+ module RecurringSelect
2
+ class Engine < Rails::Engine
3
+
4
+ initializer "recurring_select.extending_form_builder" do |app|
5
+ # config.to_prepare do
6
+ ActionView::Helpers::FormHelper.send(:include, RecurringSelectHelper::FormHelper)
7
+ ActionView::Helpers::FormOptionsHelper.send(:include, RecurringSelectHelper::FormOptionsHelper)
8
+ ActionView::Helpers::FormBuilder.send(:include, RecurringSelectHelper::FormBuilder)
9
+ end
10
+
11
+ initializer "recurring_select.connecting_middleware" do |app|
12
+ app.middleware.use RecurringSelectMiddleware # insert_after ActionDispatch::ParamsParser,
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,3 @@
1
+ module RecurringSelect
2
+ VERSION = "1.0.0"
3
+ end
metadata ADDED
@@ -0,0 +1,163 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: recurring_select
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jobber
9
+ - Forrest Zeisler
10
+ - Nathan Youngman
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+ date: 2013-04-16 00:00:00.000000000 Z
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: rails
18
+ requirement: !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ! '>='
22
+ - !ruby/object:Gem::Version
23
+ version: '3.1'
24
+ type: :runtime
25
+ prerelease: false
26
+ version_requirements: !ruby/object:Gem::Requirement
27
+ none: false
28
+ requirements:
29
+ - - ! '>='
30
+ - !ruby/object:Gem::Version
31
+ version: '3.1'
32
+ - !ruby/object:Gem::Dependency
33
+ name: jquery-rails
34
+ requirement: !ruby/object:Gem::Requirement
35
+ none: false
36
+ requirements:
37
+ - - ! '>='
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ none: false
44
+ requirements:
45
+ - - ! '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ - !ruby/object:Gem::Dependency
49
+ name: ice_cube
50
+ requirement: !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ! '>='
54
+ - !ruby/object:Gem::Version
55
+ version: '0.8'
56
+ type: :runtime
57
+ prerelease: false
58
+ version_requirements: !ruby/object:Gem::Requirement
59
+ none: false
60
+ requirements:
61
+ - - ! '>='
62
+ - !ruby/object:Gem::Version
63
+ version: '0.8'
64
+ - !ruby/object:Gem::Dependency
65
+ name: sass-rails
66
+ requirement: !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ! '>='
70
+ - !ruby/object:Gem::Version
71
+ version: '3.1'
72
+ type: :runtime
73
+ prerelease: false
74
+ version_requirements: !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ! '>='
78
+ - !ruby/object:Gem::Version
79
+ version: '3.1'
80
+ - !ruby/object:Gem::Dependency
81
+ name: coffee-rails
82
+ requirement: !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '3.1'
88
+ type: :runtime
89
+ prerelease: false
90
+ version_requirements: !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - ! '>='
94
+ - !ruby/object:Gem::Version
95
+ version: '3.1'
96
+ - !ruby/object:Gem::Dependency
97
+ name: pry
98
+ requirement: !ruby/object:Gem::Requirement
99
+ none: false
100
+ requirements:
101
+ - - ! '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ none: false
108
+ requirements:
109
+ - - ! '>='
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ description: This gem provides a useful interface for creating recurring rules for
113
+ the ice_cube gem.
114
+ email:
115
+ - forrest@getjobber.com
116
+ executables: []
117
+ extensions: []
118
+ extra_rdoc_files: []
119
+ files:
120
+ - app/assets/images/recurring_select/cancel.png
121
+ - app/assets/images/recurring_select/throbber_13x13.gif
122
+ - app/assets/javascripts/jquery-mobile-rs.js.coffee
123
+ - app/assets/javascripts/recurring_select.js.coffee
124
+ - app/assets/javascripts/recurring_select_dialog.js.coffee.erb
125
+ - app/assets/stylesheets/jquery-mobile-rs.css.scss.erb
126
+ - app/assets/stylesheets/recurring_select.css.scss.erb
127
+ - app/assets/stylesheets/utilities.scss
128
+ - app/assets/templates/recurring_select/dialog_template.html
129
+ - app/helpers/recurring_select_helper.rb
130
+ - app/middleware/recurring_select_middleware.rb
131
+ - config/routes.rb
132
+ - lib/recurring_select/engine.rb
133
+ - lib/recurring_select/version.rb
134
+ - lib/recurring_select.rb
135
+ - MIT-LICENSE
136
+ - Rakefile
137
+ - README.md
138
+ homepage: http://github.com/getjobber/recurring_select
139
+ licenses: []
140
+ post_install_message:
141
+ rdoc_options: []
142
+ require_paths:
143
+ - lib
144
+ required_ruby_version: !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - ! '>='
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ required_rubygems_version: !ruby/object:Gem::Requirement
151
+ none: false
152
+ requirements:
153
+ - - ! '>='
154
+ - !ruby/object:Gem::Version
155
+ version: '0'
156
+ requirements: []
157
+ rubyforge_project:
158
+ rubygems_version: 1.8.24
159
+ signing_key:
160
+ specification_version: 3
161
+ summary: A select helper which gives you magical powers to generate ice_cube rules.
162
+ test_files: []
163
+ has_rdoc: