From 56bf9d9b94f0759b198e675ebc0ab5b64861e5c6 Mon Sep 17 00:00:00 2001 From: Jimmy Date: Mon, 5 Aug 2024 14:33:46 -0700 Subject: [PATCH] add support for recurrence termination options based on changes by rflorence (https://github.com/gregschmit/recurring_select/pull/117) --- app/assets/javascripts/defaults.js | 5 + app/assets/javascripts/recurring_select.js | 1 + .../recurring_select_dialog.js.erb | 114 +++++++++++++++--- app/assets/stylesheets/recurring_select.scss | 74 +++++++++++- lib/recurring_select.rb | 18 +++ recurring_select.gemspec | 1 + .../app/assets/javascripts/application.js | 2 +- .../app/assets/stylesheets/application.scss | 1 + spec/dummy/config/application.rb | 2 + 9 files changed, 201 insertions(+), 17 deletions(-) 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..c3e12ff9 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,38 @@ 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" + }); + + 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 +248,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 +284,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,7 +314,8 @@ 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; @@ -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(); } 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..6c6be206 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,41 @@ select { } } + // New styles for termination section + .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: 80px; + text-align: center; + } + + // New styles for date range inputs + .date_range { + margin-bottom: 10px; + display: flex; + align-items: center; + + label { + margin-right: 5px; + } + + input[type="text"] { + width: 150px; + padding: 5px; + margin-right: 10px; + } + } .rs_summary { padding: 0px; @@ -129,3 +163,41 @@ select { } } + +// Flatpickr styles +.flatpickr-calendar { + background: #fff; + border: 1px solid #e6e6e6; + box-shadow: 0 3px 13px rgba(0,0,0,0.08); +} + +.flatpickr-day { + &.selected, + &.startRange, + &.endRange, + &.selected.inRange, + &.startRange.inRange, + &.endRange.inRange, + &.selected:focus, + &.startRange:focus, + &.endRange:focus, + &.selected:hover, + &.startRange:hover, + &.endRange:hover, + &.selected.prevMonthDay, + &.startRange.prevMonthDay, + &.endRange.prevMonthDay, + &.selected.nextMonthDay, + &.startRange.nextMonthDay, + &.endRange.nextMonthDay { + background: #89a; + border-color: #89a; + } +} + +.flatpickr-time input:hover, +.flatpickr-time .flatpickr-am-pm:hover, +.flatpickr-time input:focus, +.flatpickr-time .flatpickr-am-pm:focus { + background: #eee; +} 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/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..6ca5c043 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.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