recurring_select 1.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.
@@ -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: