diff --git a/Gemfile b/Gemfile index f9e1be74..f6200b45 100644 --- a/Gemfile +++ b/Gemfile @@ -5,6 +5,7 @@ gem 'pg', platform: :ruby gem 'activerecord-jdbcpostgresql-adapter', platform: :jruby gem 'jquery-rails' +gem "jquery-ui-rails" gem 'rails', '~> 4.0.12' gem 'sass-rails', '~> 4.0.5' diff --git a/Gemfile.lock b/Gemfile.lock index 70357775..b981e979 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -37,28 +37,32 @@ GEM tzinfo (~> 0.3.37) arel (4.0.2) builder (3.1.4) - coffee-rails (4.1.0) + coffee-rails (4.1.1) coffee-script (>= 2.2.0) - railties (>= 4.0.0, < 5.0) + railties (>= 4.0.0, < 5.1.x) coffee-script (2.4.1) coffee-script-source execjs - coffee-script-source (1.9.1.1) + coffee-script-source (1.10.0) diff-lcs (1.2.5) erubis (2.7.0) - execjs (2.6.0) + execjs (2.7.0) hike (1.2.3) i18n (0.7.0) - ice_cube (0.13.0) - jquery-rails (3.1.2) + ice_cube (0.14.0) + jquery-rails (3.1.4) railties (>= 3.0, < 5.0) thor (>= 0.14, < 2.0) - mail (2.6.3) - mime-types (>= 1.16, < 3) - mime-types (2.6.2) + jquery-ui-rails (5.0.5) + railties (>= 3.2.16) + mail (2.6.4) + mime-types (>= 1.16, < 4) + mime-types (3.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2016.0521) minitest (4.7.5) - multi_json (1.11.2) - pg (0.17.1) + multi_json (1.12.1) + pg (0.18.4) rack (1.5.5) rack-test (0.6.3) rack (>= 1.0) @@ -75,27 +79,28 @@ GEM activesupport (= 4.0.13) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rake (10.4.2) - rspec (3.1.0) - rspec-core (~> 3.1.0) - rspec-expectations (~> 3.1.0) - rspec-mocks (~> 3.1.0) - rspec-core (3.1.7) - rspec-support (~> 3.1.0) - rspec-expectations (3.1.2) + rake (11.1.2) + rspec (3.4.0) + rspec-core (~> 3.4.0) + rspec-expectations (~> 3.4.0) + rspec-mocks (~> 3.4.0) + rspec-core (3.4.4) + rspec-support (~> 3.4.0) + rspec-expectations (3.4.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.1.0) - rspec-mocks (3.1.3) - rspec-support (~> 3.1.0) - rspec-rails (3.1.0) - actionpack (>= 3.0) - activesupport (>= 3.0) - railties (>= 3.0) - rspec-core (~> 3.1.0) - rspec-expectations (~> 3.1.0) - rspec-mocks (~> 3.1.0) - rspec-support (~> 3.1.0) - rspec-support (3.1.2) + rspec-support (~> 3.4.0) + rspec-mocks (3.4.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.4.0) + rspec-rails (3.4.2) + actionpack (>= 3.0, < 4.3) + activesupport (>= 3.0, < 4.3) + railties (>= 3.0, < 4.3) + rspec-core (~> 3.4.0) + rspec-expectations (~> 3.4.0) + rspec-mocks (~> 3.4.0) + rspec-support (~> 3.4.0) + rspec-support (3.4.1) sass (3.2.19) sass-rails (4.0.5) railties (>= 4.0.0, < 5.0) @@ -114,7 +119,7 @@ GEM thor (0.19.1) thread_safe (0.3.5) tilt (1.4.1) - tzinfo (0.3.44) + tzinfo (0.3.49) PLATFORMS ruby @@ -123,6 +128,7 @@ DEPENDENCIES activerecord-jdbcpostgresql-adapter bundler (>= 1.3.5) jquery-rails + jquery-ui-rails pg rails (~> 4.0.12) rake (>= 0.9.6) diff --git a/README.md b/README.md index 8d526d9e..f2764158 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,11 @@ Use :allow_blank for a "not recurring" option: f.select_recurring :current_existing_rule, nil, :allow_blank => true ``` +Use :data attribute to position the recurring select dialog inline (after the select input): + +```ruby + f.select_recurring :current_existing_rule, nil, { :allow_blank => true }, { data: { recurring_select_position: 'inline' } } +``` ### Additional Helpers @@ -130,7 +135,15 @@ $.fn.recurring_select.options = { monthly: { show_week: [true, true, true, true, false, false] //display week 1, 2 .... Last } + until: true //require until date + indefinite_until: true //until be set to indefinite + datepicker: { + firstDay: 1, # 1 = monday, 0 = sunday + beforeShowDayFunction: -> + }, + close_on_outside_click: true } + ``` ## Testing and Development diff --git a/app/assets/javascripts/recurring_select.js.coffee b/app/assets/javascripts/recurring_select.js.coffee index d4932139..7645527a 100644 --- a/app/assets/javascripts/recurring_select.js.coffee +++ b/app/assets/javascripts/recurring_select.js.coffee @@ -1,3 +1,4 @@ +//= require jquery-ui //= require recurring_select_dialog //= require_self @@ -76,7 +77,14 @@ $.fn.recurring_select = (method) -> $.fn.recurring_select.options = { monthly: { show_week: [true, true, true, true, false, false] - } + }, + until: true, + indefinite_until: true, + datepicker: { + firstDay: 1, # 1 = monday, 0 = sunday + beforeShowDayFunction: -> + }, + close_on_outside_click: true } $.fn.recurring_select.texts = { @@ -97,6 +105,8 @@ $.fn.recurring_select.texts = { day_of_week: "Day of week" cancel: "Cancel" ok: "OK" + until: "Until" + repeats_indefinitely: "Repeats indefinitely" summary: "Summary" first_day_of_week: 0 days_first_letter: ["S", "M", "T", "W", "T", "F", "S" ] diff --git a/app/assets/javascripts/recurring_select_dialog.js.coffee.erb b/app/assets/javascripts/recurring_select_dialog.js.coffee.erb index baeccd6a..1e343672 100644 --- a/app/assets/javascripts/recurring_select_dialog.js.coffee.erb +++ b/app/assets/javascripts/recurring_select_dialog.js.coffee.erb @@ -2,25 +2,31 @@ window.RecurringSelectDialog = class RecurringSelectDialog constructor: (@recurring_selector) -> @current_rule = @recurring_selector.recurring_select('current_rule') + @position = @recurring_selector.data('recurring-select-position') @initDialogBox() if not @current_rule.hash? or not @current_rule.hash.rule_type? @freqChanged() - else + else if @position != 'inline' setTimeout @positionDialogVert, 10 # allow initial render initDialogBox: -> $(".rs_dialog_holder").remove() - open_in = $("body") + open_in = if @position == 'inline' + @recurring_selector.parent() + else + $("body") open_in = $(".ui-page-active") if $(".ui-page-active").length open_in.append @template() @outer_holder = $(".rs_dialog_holder") + @outer_holder.addClass @position @inner_holder = @outer_holder.find ".rs_dialog" @content = @outer_holder.find ".rs_dialog_content" - @positionDialogVert(true) + @positionDialogVert(true) unless @position == 'inline' @mainEventInit() @freqInit() @summaryInit() + @untilInit() @outer_holder.trigger "recurring_select:dialog_opened" @freq_select.focus() @@ -49,18 +55,45 @@ window.RecurringSelectDialog = @content.css {"width": "auto"} @inner_holder.trigger "recurring_select:dialog_positioned" - cancel: => + cancel: (e) => + e.preventDefault() if e @outer_holder.remove() @recurring_selector.recurring_select('cancel') + $('body').off('click.recurring_select_cancel') outerCancel: (event) => - if $(event.target).hasClass("rs_dialog_holder") + if $.fn.recurring_select.options.close_on_outside_click && $(event.target).hasClass("rs_dialog_holder") @cancel() save: => return if !@current_rule.str? + valid = true + + if @current_rule.hash?.rule_type == 'IceCube::WeeklyRule' && !@current_rule.hash?.validations? + @content.find('.weekly_options').addClass 'rs_error' + valid = false + + if @current_rule.hash?.rule_type == 'IceCube::MonthlyRule' + week_mode = @content.find(".monthly_rule_type_week").prop("checked") + + if week_mode && $.isEmptyObject(@current_rule.hash?.validations.day_of_week) + @content.find('.monthly_options .rs_calendar_week').addClass 'rs_error' + valid = false + else if !week_mode && @current_rule.hash?.validations.day_of_month.length < 1 + @content.find('.monthly_options .rs_calendar_day').addClass 'rs_error' + valid = false + + + if !$.fn.recurring_select.options.indefinite_until && $.fn.recurring_select.options.until && !@current_rule.hash?.until? + @content.find('input.rs_until').addClass 'rs_error' + valid = false + + if !valid + return false + @outer_holder.remove() @recurring_selector.recurring_select('save', @current_rule) + $('body').off('click.recurring_select_cancel') # ========================= Init Methods =============================== @@ -70,6 +103,8 @@ window.RecurringSelectDialog = @content.on 'click tap', 'h1 a', @cancel @save_button = @content.find('input.rs_save').on "click tap", @save @content.find('input.rs_cancel').on "click tap", @cancel + @content.find('input.rs_until').on "change", @untilChanged + @content.find('input.rs_indefinite').on 'change tap', @indefiniteChanged freqInit: -> @freq_select = @outer_holder.find ".rs_frequency" @@ -176,7 +211,18 @@ window.RecurringSelectDialog = @current_rule.str = data @summaryUpdate() @content.css {"width": "auto"} - + + untilInit: => + @content.find('input.rs_until').datepicker + firstDay: $.fn.recurring_select.options.datepicker.firstDay + dateFormat: '<%= RecurringSelect.datepicker_format %>' + beforeShowDay: $.fn.recurring_select.options.datepicker.beforeShowDayFunction + onSelect: (date) => + @setUntil date + .val @current_rule.hash?.until + if $.fn.recurring_select.options.indefinite_until + @content.find('input.rs_indefinite').click() if @current_rule.hash?.until? + init_calendar_days: (section) => monthly_calendar = section.find(".rs_calendar_day") monthly_calendar.html "" @@ -223,15 +269,16 @@ window.RecurringSelectDialog = # ========================= Change callbacks =============================== freqChanged: => + old_until = @current_rule.hash?.until @current_rule.hash = null unless $.isPlainObject(@current_rule.hash) # for custom values @current_rule.hash ||= {} @current_rule.hash.interval = 1 - @current_rule.hash.until = null + @current_rule.hash.until = old_until @current_rule.hash.count = null @current_rule.hash.validations = null @content.find(".freq_option_section").hide(); - @content.find("input[type=radio], input[type=checkbox]").prop("checked", false) + @content.find("input[type=radio], input[type=checkbox]:not(.rs_indefinite)").prop("checked", false) switch @freq_select.val() when "Weekly" @current_rule.hash.rule_type = "IceCube::WeeklyRule" @@ -249,9 +296,10 @@ window.RecurringSelectDialog = @current_rule.hash.rule_type = "IceCube::DailyRule" @current_rule.str = $.fn.recurring_select.texts["daily"] @initDailyOptions() + @current_rule.str = null @summaryUpdate() - @positionDialogVert() - + @positionDialogVert() unless @position == 'inline' + intervalChanged: (event) => @current_rule.str = null @current_rule.hash ||= {} @@ -266,6 +314,10 @@ window.RecurringSelectDialog = @current_rule.hash ||= {} @current_rule.hash.validations = {} raw_days = @content.find(".day_holder a.selected").map -> parseInt($(this).data("value")) + + if raw_days.length > 0 + @content.find(".weekly_options").removeClass('rs_error') + @current_rule.hash.validations.day = raw_days.get() @summaryUpdate() false # this prevents default and propogation @@ -278,6 +330,10 @@ window.RecurringSelectDialog = raw_days = @content.find(".monthly_options .rs_calendar_day a.selected").map -> res = if $(this).text() == $.fn.recurring_select.texts["last_day"] then -1 else parseInt($(this).text()) res + + if raw_days.length > 0 + @content.find(".monthly_options .rs_calendar_day").removeClass('rs_error') + @current_rule.hash.validations.day_of_week = {} @current_rule.hash.validations.day_of_month = raw_days.get() @summaryUpdate() @@ -290,14 +346,60 @@ window.RecurringSelectDialog = @current_rule.hash.validations = {} @current_rule.hash.validations.day_of_month = [] @current_rule.hash.validations.day_of_week = {} - @content.find(".monthly_options .rs_calendar_week a.selected").each (index, elm) => + raw_days = @content.find(".monthly_options .rs_calendar_week a.selected") + raw_days.each (index, elm) => day = parseInt($(elm).attr("day")) instance = parseInt($(elm).attr("instance")) @current_rule.hash.validations.day_of_week[day] ||= [] @current_rule.hash.validations.day_of_week[day].push instance + + if raw_days.length > 0 + @content.find(".monthly_options .rs_calendar_week").removeClass('rs_error') + @summaryUpdate() false + untilChanged: (event) => + @setUntil $(event.currentTarget).val() + + setUntil: (date) -> + @current_rule.hash ||= {} + @current_rule.str = null + @current_rule.hash.until = date + @summaryUpdate() + false + + indefiniteChanged: (event) => + $el = $(event.currentTarget) + $untilSection = $el.parents('.until_option').find('.until_input') + $untilInput = $untilSection.find 'input.rs_until' + if $el.is ':checked' + $untilInput.val('').blur() + $untilSection.hide() + if @current_rule.hash + delete @current_rule.hash.until + delete @current_rule.str + else + if @current_rule.hash?.until + $untilInput.val @current_rule.hash.until + else + $untilInput.datepicker('setDate', @getUntilDefaultDate()) + .focus + $untilInput.blur() + $untilSection.show() + @summaryUpdate() + + getUntilDefaultDate: => + today = new Date + year = today.getFullYear() + month = today.getMonth() + day = today.getDate() + switch @current_rule.hash.rule_type + when "IceCube::MonthlyRule" then new Date year + 1, month, day + when "IceCube::YearlyRule" then new Date year + 5, month, day + when "IceCube::WeeklyRule" then new Date year, month + 1, day + else new Date year, month, day + 7 + # ========================= Change callbacks =============================== template: () -> @@ -306,9 +408,9 @@ window.RecurringSelectDialog =

#{$.fn.recurring_select.texts["repeat"]}

-

- - @@ -317,16 +419,53 @@ window.RecurringSelectDialog =

-

+

+ #{$.fn.recurring_select.texts["every"]} - + #{$.fn.recurring_select.texts["days"]}

-

+

+ #{$.fn.recurring_select.texts["every"]} - + #{$.fn.recurring_select.texts["weeks_on"]}:

@@ -340,9 +479,23 @@ window.RecurringSelectDialog = .
-

+

+ #{$.fn.recurring_select.texts["every"]} - + #{$.fn.recurring_select.texts["months"]}:

@@ -353,12 +506,47 @@ window.RecurringSelectDialog =

-

+

+ #{$.fn.recurring_select.texts["every"]} - + #{$.fn.recurring_select.texts["years"]}

-
+
" + + if $.fn.recurring_select.options.until + + str +="
" + + if $.fn.recurring_select.options.indefinite_until + str +="

+ +

" + + + str += "

+ + +

+
" + + str += "

@@ -369,4 +557,4 @@ window.RecurringSelectDialog =
- " + " \ No newline at end of file diff --git a/app/assets/stylesheets/recurring_select.scss b/app/assets/stylesheets/recurring_select.scss index 4d5bd72f..ce5624bc 100644 --- a/app/assets/stylesheets/recurring_select.scss +++ b/app/assets/stylesheets/recurring_select.scss @@ -28,9 +28,15 @@ select { option.bold {font-weight:bold; color:red;} } -.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; +.rs_dialog_holder { + background-color:rgba(255,255,255,0.2); + + &.fixed { + position:fixed; left:0px; right:0px; top:0px; bottom:0px; padding-left:50%; z-index:50; + .rs_dialog { margin-left:-125px; } + } .rs_dialog { background-color:#f6f6f6; border:1px solid #acacac; @include shadows(1px, 3px, 8px, rgba(0,0,0,0.25)); @include rounded_corners(7px); - display:inline-block; min-width:200px; margin-left:-125px; overflow:hidden; position:relative; + display:inline-block; min-width:200px; overflow:hidden; position:relative; .rs_dialog_content { padding:10px; h1 { font-size:16px; padding:0px; margin:0 0 10px 0; a {float:right; display:inline-block; height:16px; width:16px; background-image:image-url("recurring_select/cancel.png"); background-position:center; background-repeat:no-repeat;} @@ -100,4 +106,10 @@ select { } } + + input { + &.rs_error { + border-color: red; + } + } } diff --git a/app/helpers/recurring_select_helper.rb b/app/helpers/recurring_select_helper.rb index b4fc7e70..0908f661 100644 --- a/app/helpers/recurring_select_helper.rb +++ b/app/helpers/recurring_select_helper.rb @@ -2,7 +2,7 @@ module RecurringSelectHelper module FormHelper - if Rails::VERSION::MAJOR == 4 + if Rails::VERSION::MAJOR == 4 || Rails::VERSION::MAJOR == 5 def select_recurring(object, method, default_schedules = nil, options = {}, html_options = {}) RecurringSelectTag.new(object, method, self, default_schedules, options, html_options).render end @@ -68,7 +68,7 @@ def ice_cube_rule_to_option(supplied_rule, custom = false) return supplied_rule unless RecurringSelect.is_valid_rule?(supplied_rule) rule = RecurringSelect.dirty_hash_to_rule(supplied_rule) - ar = [rule.to_s, rule.to_hash.to_json] + ar = [rule.to_s, rule_as_json(rule)] if custom ar[0] << "*" @@ -77,6 +77,14 @@ def ice_cube_rule_to_option(supplied_rule, custom = false) ar end + + def rule_as_json(rule) + hash = rule.to_hash + if hash.delete(:until) + hash[:until] = rule.until_time.to_date + end + hash.to_json + end def current_rule_in_defaults?(currently_selected_rule, default_schedules) default_schedules.any?{|option| diff --git a/lib/recurring_select.rb b/lib/recurring_select.rb index 5f727ed4..4b2357a1 100644 --- a/lib/recurring_select.rb +++ b/lib/recurring_select.rb @@ -2,7 +2,34 @@ require "ice_cube" module RecurringSelect - + + class << self + attr_writer :date_format + + def date_format + @date_format || '%Y-%m-%d' + end + + # Convert from format to jQuery UI datepicker date format + def datepicker_format + datepicker_format = String.new date_format + @datepicker_mappings.each { |k, v| datepicker_format[k] &&= v } + datepicker_format + end + + end + + @datepicker_mappings = { + '%Y' => 'yy', + '%y' => 'y', + '%m' => 'mm', + '%-m' => 'm', + '%d' => 'dd', + '%-d' => 'd', + '%D' => 'mm/dd/y', + '%x' => 'mm/dd/y' + } + def self.dirty_hash_to_rule(params) if params.is_a? IceCube::Rule params @@ -45,7 +72,23 @@ def self.filter_params(params) params[:interval] = params[:interval].to_i if params[:interval] params[:week_start] = params[:week_start].to_i if params[:week_start] - + begin + # IceCube::TimeUtil will serialize a TimeWithZone into a hash, such as: + # {time: Thu, 04 Sep 2014 06:59:59 +0000, zone: "Pacific Time (US & Canada)"} + # So don't try to DateTime.parse the hash. IceCube::TimeUtil will deserialize this for us. + if (until_param = params[:until]) + if until_param.is_a?(String) + params[:until] = Time.strptime(params[:until], self.date_format) + # Set to 23:59:59 (in current TZ) to encompass all events on until day + params[:until] = Time.zone.parse(params[:until].to_s).change(hour: 23, min: 59, sec: 59) + elsif until_param.is_a?(Hash) # ex: {time: Thu, 28 Aug 2014 06:59:59 +0000, zone: "Pacific Time (US & Canada)"} + params[:until] = until_param[:time].in_time_zone(until_param[:zone]) + end + end + rescue ArgumentError + # Invalid date given, attempt to assign :until will fail silently + end + params[:validations] ||= {} params[:validations].symbolize_keys! diff --git a/spec/dummy/app/assets/javascripts/application.js b/spec/dummy/app/assets/javascripts/application.js index 4d3fda77..91d8129b 100644 --- a/spec/dummy/app/assets/javascripts/application.js +++ b/spec/dummy/app/assets/javascripts/application.js @@ -9,8 +9,8 @@ //= require recurring_select //= require_tree . -$.fn.recurring_select.options = { - monthly: { - show_week: [true, true, true, true, true, true] - } -}; +//$.fn.recurring_select.options = { +// monthly: { +// show_week: [true, true, true, true, true, true] +// } +//}; diff --git a/spec/dummy/app/assets/stylesheets/application.scss b/spec/dummy/app/assets/stylesheets/application.scss index d03ff6d8..d013b30c 100644 --- a/spec/dummy/app/assets/stylesheets/application.scss +++ b/spec/dummy/app/assets/stylesheets/application.scss @@ -4,6 +4,7 @@ * the top of the compiled file, but it's generally better to create a new file per style scope. *= require_self *= require recurring_select + *= require jquery-ui */