diff --git a/app/assets/javascripts/defaults.js b/app/assets/javascripts/defaults.js index 3386fad9..aa906d75 100644 --- a/app/assets/javascripts/defaults.js +++ b/app/assets/javascripts/defaults.js @@ -20,6 +20,11 @@ const defaultConfig = { years: "year(s)", day_of_month: "Day of month", day_of_week: "Day of week", + ends: "Ends", + never: "Never", + after: "After", + occurrences: "occurrences", + on: "On", cancel: "Cancel", ok: "OK", summary: "Summary", diff --git a/app/assets/javascripts/recurring_select.js b/app/assets/javascripts/recurring_select.js index c93e6bd1..c4498c16 100644 --- a/app/assets/javascripts/recurring_select.js +++ b/app/assets/javascripts/recurring_select.js @@ -13,6 +13,7 @@ document.addEventListener("DOMContentLoaded", () => { recurring_select.call(e.target, "changed") } }) + }) const methods = { diff --git a/app/assets/javascripts/recurring_select_dialog.js.erb b/app/assets/javascripts/recurring_select_dialog.js.erb index bd60f62a..2fe2ed68 100644 --- a/app/assets/javascripts/recurring_select_dialog.js.erb +++ b/app/assets/javascripts/recurring_select_dialog.js.erb @@ -17,6 +17,7 @@ class RecurringSelectDialog { this.daysChanged = this.daysChanged.bind(this); this.dateOfMonthChanged = this.dateOfMonthChanged.bind(this); this.weekOfMonthChanged = this.weekOfMonthChanged.bind(this); + this.terminationChanged = this.terminationChanged.bind(this); this.recurring_selector = recurring_selector; this.current_rule = this.recurring_selector.recurring_select('current_rule'); this.initDialogBox(); @@ -41,9 +42,12 @@ class RecurringSelectDialog { this.mainEventInit(); this.freqInit(); + this.terminationInit(); this.summaryInit(); + trigger(this.outer_holder, "recurring_select:dialog_opened"); this.freq_select.focus(); + } cancel() { @@ -134,9 +138,9 @@ class RecurringSelectDialog { interval_input.value = this.current_rule.hash.interval on(interval_input, "change keyup", this.intervalChanged) - if (!this.current_rule.hash.validations) { this.current_rule.hash.validations = {} }; - if (!this.current_rule.hash.validations.day_of_month) { this.current_rule.hash.validations.day_of_month = [] }; - if (!this.current_rule.hash.validations.day_of_week) { this.current_rule.hash.validations.day_of_week = {} }; + if (!this.current_rule.hash.validations) { this.current_rule.hash.validations = {}; } + if (!this.current_rule.hash.validations.day_of_month) { this.current_rule.hash.validations.day_of_month = []; } + if (!this.current_rule.hash.validations.day_of_week) { this.current_rule.hash.validations.day_of_week = {}; } this.init_calendar_days(section); this.init_calendar_weeks(section); @@ -156,6 +160,40 @@ class RecurringSelectDialog { section.style.display = 'block' } + terminationInit() { + const section = this.outer_holder.querySelector(".rs_termination_section"); + this.until_date = section.querySelector("#rs_until_date"); + this.until_date.flatpickr({ + enableTime: true, + dateFormat: "Y-m-d H:i", + altInput: true, + altFormat: "F j, Y h:i K", + }); + + if (this.current_rule.hash && this.current_rule.hash.count) { + this.count_option = section.querySelector("input[name=rs_termination][value=count]"); + this.count_option.checked = true; + this.occurence_count = section.querySelector("#rs_occurrence_count"); + this.occurence_count.value = this.current_rule.hash.count; + } else if (this.current_rule.hash && this.current_rule.hash.until) { + this.until_option = section.querySelector("input[name=rs_termination][value=until]"); + this.until_option.checked = true; + // 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)"} + // If we're initializing from an unsaved rule, until will be a string + if (this.current_rule.hash.until.time) { + this.until_val = new Date(this.current_rule.hash.until.time); + this.until_date.value = (this.until_val.getFullYear() + "-" + (this.until_val.getMonth() + 1) + "-" + this.until_val.getDate() + " " + this.until_val.getHours() + ":" + this.until_val.getMinutes()); + } else { + this.until_date.value = this.current_rule.hash.until; + } + } else { + this.never_option = section.querySelector("input[name=rs_termination][value=never]"); + this.never_option.checked = true; + } + + section.addEventListener("change", this.terminationChanged.bind(this)); + } summaryInit() { this.summary = this.outer_holder.querySelector(".rs_summary"); @@ -212,7 +250,7 @@ class RecurringSelectDialog { if (Array.from(this.current_rule.hash.validations.day_of_month).includes(num)) { day_link.classList.add("selected"); } - }; + } // add last day of month button const end_of_month_link = document.createElement("a") @@ -248,9 +286,9 @@ class RecurringSelectDialog { day_link.setAttribute("day", day_of_week); day_link.setAttribute("instance", num); monthly_calendar.appendChild(day_link); - }; + } } - }; + } Object.entries(this.current_rule.hash.validations.day_of_week).forEach(([key, value]) => { Array.from(value).forEach((instance, index) => { @@ -278,10 +316,9 @@ class RecurringSelectDialog { freqChanged() { if (!isPlainObject(this.current_rule.hash)) { this.current_rule.hash = null; } // for custom values - if (!this.current_rule.hash) { this.current_rule.hash = {} }; + if (!this.current_rule.hash) { this.current_rule.hash = {} } + this.current_rule.str = null; this.current_rule.hash.interval = 1; - this.current_rule.hash.until = null; - this.current_rule.hash.count = null; this.current_rule.hash.validations = null; this.content.querySelectorAll(".freq_option_section").forEach(el => el.style.display = 'none') this.content.querySelector("input[type=radio], input[type=checkbox]").checked = false @@ -305,13 +342,13 @@ class RecurringSelectDialog { this.current_rule.hash.rule_type = "IceCube::DailyRule"; this.current_rule.str = this.config.texts["daily"]; this.initDailyOptions(); - }; - this.summaryUpdate(); + } + this.summaryFetch(); } intervalChanged(event) { this.current_rule.str = null; - if (!this.current_rule.hash) { this.current_rule.hash = {} }; + if (!this.current_rule.hash) { this.current_rule.hash = {}; } this.current_rule.hash.interval = parseInt(event.currentTarget.value); if ((this.current_rule.hash.interval < 1) || isNaN(this.current_rule.hash.interval)) { this.current_rule.hash.interval = 1; @@ -322,7 +359,7 @@ class RecurringSelectDialog { daysChanged(event) { event.target.classList.toggle("selected"); this.current_rule.str = null; - if (!this.current_rule.hash) { this.current_rule.hash = {} }; + if (!this.current_rule.hash) { this.current_rule.hash = {}; } this.current_rule.hash.validations = {}; const raw_days = Array.from(this.content.querySelectorAll(".day_holder a.selected")) .map(el => parseInt(el.dataset.value)) @@ -334,7 +371,7 @@ class RecurringSelectDialog { dateOfMonthChanged(event) { event.target.classList.toggle("selected"); this.current_rule.str = null; - if (!this.current_rule.hash) { this.current_rule.hash = {} }; + if (!this.current_rule.hash) { this.current_rule.hash = {}; } this.current_rule.hash.validations = {}; const raw_days = Array.from(this.content.querySelectorAll(".monthly_options .rs_calendar_day a.selected")) .map(el => { @@ -349,7 +386,7 @@ class RecurringSelectDialog { weekOfMonthChanged(event) { event.target.classList.toggle("selected"); this.current_rule.str = null; - if (!this.current_rule.hash) { this.current_rule.hash = {} }; + if (!this.current_rule.hash) { this.current_rule.hash = {}; } this.current_rule.hash.validations = {}; this.current_rule.hash.validations.day_of_month = []; this.current_rule.hash.validations.day_of_week = {}; @@ -357,13 +394,36 @@ class RecurringSelectDialog { .forEach((elm, index) => { const day = parseInt(elm.getAttribute("day")); const instance = parseInt(elm.getAttribute("instance")); - if (!this.current_rule.hash.validations.day_of_week[day]) { this.current_rule.hash.validations.day_of_week[day] = [] }; + if (!this.current_rule.hash.validations.day_of_week[day]) { this.current_rule.hash.validations.day_of_week[day] = []; } return this.current_rule.hash.validations.day_of_week[day].push(instance); }) this.summaryUpdate(); return false; } + terminationChanged() { + this.selected_termination_type = this.outer_holder.querySelector(".rs_termination_section input[type='radio']:checked"); + if (!this.selected_termination_type) { return; } + this.current_rule.str = null; + if (!this.current_rule.hash) { this.current_rule.hash = {}; } + switch (this.selected_termination_type.value) { + case "count": + this.current_rule.hash.count = parseInt(this.occurence_count ? this.occurence_count.value : this.outer_holder.querySelector("#rs_occurrence_count").value); + if ((this.current_rule.hash.count < 1) || isNaN(this.current_rule.hash.count)) { + this.current_rule.hash.count = 1; + } + this.current_rule.hash.until = null; + break + case "until": + this.current_rule.hash.until = this.until_date ? this.until_date.value : this.outer_holder.querySelector("#rs_until_date").value; + this.current_rule.hash.count = null; + break + default: + this.current_rule.hash.count = null; + this.current_rule.hash.until = null; + } + this.summaryUpdate(); + } // ========================= Change callbacks =============================== template() { @@ -381,7 +441,6 @@ class RecurringSelectDialog { \ \

\ - \
\

\ ${this.config.texts["every"]} \ @@ -400,7 +459,7 @@ class RecurringSelectDialog { for (let i = this.config.texts["first_day_of_week"], day_of_week = i, end = 7 + this.config.texts["first_day_of_week"], asc = this.config.texts["first_day_of_week"] <= end; asc ? i < end : i > end; asc ? i++ : i--, day_of_week = i) { day_of_week = day_of_week % 7; str += `${this.config.texts["days_first_letter"][day_of_week]}`; - }; + } str += `\

\ @@ -426,6 +485,31 @@ class RecurringSelectDialog { ${this.config.texts["years"]} \

\ \ +
+ + + + +
+ + +
+
+ +
+

\ \

\ diff --git a/app/assets/stylesheets/recurring_select.scss b/app/assets/stylesheets/recurring_select.scss index 67d7154d..294678db 100644 --- a/app/assets/stylesheets/recurring_select.scss +++ b/app/assets/stylesheets/recurring_select.scss @@ -1,5 +1,4 @@ @import "utilities.scss"; - /* -------- resets ---------------*/ .rs_dialog_holder { @@ -98,6 +97,23 @@ select { } } + .rs_termination_section { + table { + margin: 0; + padding-top: 5px; + td { + padding: 0; + vertical-align: top; + } + } + .rs_termination_label {margin-right:10px;} + .rs_count {width:30px; text-align:center; display: inline-block;} + } + + .rs_datepicker { + width: 150px; + text-align: center; + } .rs_summary { padding: 0px; @@ -128,4 +144,4 @@ select { } } -} +} \ No newline at end of file diff --git a/lib/recurring_select.rb b/lib/recurring_select.rb index 7a02c735..05f707bb 100644 --- a/lib/recurring_select.rb +++ b/lib/recurring_select.rb @@ -44,6 +44,7 @@ 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] + params[:count] = params[:count].to_i if params[:count] params[:validations] ||= {} params[:validations].symbolize_keys! @@ -77,6 +78,23 @@ def self.filter_params(params) params[:validations][:day_of_year] = params[:validations][:day_of_year].collect(&:to_i) end + 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) + # Set to 23:59:59 (in current TZ) to encompass all events on until day + params[:until] = Time.zone.parse(until_param).change(hour: 23, min: 59, sec: 59) + elsif until_param.is_a?(Hash) # ex: {time: Thu, 28 Aug 2014 06:59:590000, zone: "Pacific Time (US & Canada)"} + until_param = until_param.symbolize_keys + 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 end end diff --git a/lib/recurring_select/version.rb b/lib/recurring_select/version.rb index 2db9857e..22b67d5d 100644 --- a/lib/recurring_select/version.rb +++ b/lib/recurring_select/version.rb @@ -1,3 +1,3 @@ module RecurringSelect - VERSION = "4.0.0" + VERSION = "4.0.1" end diff --git a/recurring_select.gemspec b/recurring_select.gemspec index 21fc21de..5126d58b 100644 --- a/recurring_select.gemspec +++ b/recurring_select.gemspec @@ -17,6 +17,7 @@ Gem::Specification.new do |s| s.add_dependency "rails", ">= 6.1" s.add_dependency "ice_cube", ">= 0.11" s.add_dependency "sass-rails", ">= 6.0" + s.add_dependency "flatpickr", ">= 4.6.6" s.add_development_dependency "bundler", ">= 1.3.5" s.add_development_dependency "rspec-rails", ">= 2.14" diff --git a/spec/dummy/app/assets/javascripts/application.js b/spec/dummy/app/assets/javascripts/application.js index 5253f3aa..71eeff16 100644 --- a/spec/dummy/app/assets/javascripts/application.js +++ b/spec/dummy/app/assets/javascripts/application.js @@ -4,9 +4,9 @@ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // the compiled file. // +//= require flatpickr/dist/flatpickr.min.js //= require recurring_select //= require_tree . - RecurringSelectDialog.config.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 37f51c9e..39ce3ea0 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 flatpickr */ diff --git a/spec/dummy/config/application.rb b/spec/dummy/config/application.rb index 2162cb1a..1efa5893 100644 --- a/spec/dummy/config/application.rb +++ b/spec/dummy/config/application.rb @@ -44,6 +44,8 @@ class Application < Rails::Application # Version of your assets, change this if you want to expire all your assets config.assets.version = '1.0' + + config.assets.paths << Rails.root.join('../../node_modules') end end