Skip to content

Commit d33fa70

Browse files
committed
add support for recurrence termination options based on changes by rflorence (gregschmit#117)
1 parent 340dd76 commit d33fa70

File tree

10 files changed

+150
-22
lines changed

10 files changed

+150
-22
lines changed

app/assets/javascripts/defaults.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ const defaultConfig = {
2020
years: "year(s)",
2121
day_of_month: "Day of month",
2222
day_of_week: "Day of week",
23+
ends: "Ends",
24+
never: "Never",
25+
after: "After",
26+
occurrences: "occurrences",
27+
on: "On",
2328
cancel: "Cancel",
2429
ok: "OK",
2530
summary: "Summary",

app/assets/javascripts/recurring_select.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ document.addEventListener("DOMContentLoaded", () => {
1313
recurring_select.call(e.target, "changed")
1414
}
1515
})
16+
1617
})
1718

1819
const methods = {

app/assets/javascripts/recurring_select_dialog.js.erb

Lines changed: 102 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class RecurringSelectDialog {
1717
this.daysChanged = this.daysChanged.bind(this);
1818
this.dateOfMonthChanged = this.dateOfMonthChanged.bind(this);
1919
this.weekOfMonthChanged = this.weekOfMonthChanged.bind(this);
20+
this.terminationChanged = this.terminationChanged.bind(this);
2021
this.recurring_selector = recurring_selector;
2122
this.current_rule = this.recurring_selector.recurring_select('current_rule');
2223
this.initDialogBox();
@@ -41,9 +42,12 @@ class RecurringSelectDialog {
4142

4243
this.mainEventInit();
4344
this.freqInit();
45+
this.terminationInit();
4446
this.summaryInit();
47+
4548
trigger(this.outer_holder, "recurring_select:dialog_opened");
4649
this.freq_select.focus();
50+
4751
}
4852

4953
cancel() {
@@ -134,9 +138,9 @@ class RecurringSelectDialog {
134138
interval_input.value = this.current_rule.hash.interval
135139
on(interval_input, "change keyup", this.intervalChanged)
136140

137-
if (!this.current_rule.hash.validations) { this.current_rule.hash.validations = {} };
138-
if (!this.current_rule.hash.validations.day_of_month) { this.current_rule.hash.validations.day_of_month = [] };
139-
if (!this.current_rule.hash.validations.day_of_week) { this.current_rule.hash.validations.day_of_week = {} };
141+
if (!this.current_rule.hash.validations) { this.current_rule.hash.validations = {}; }
142+
if (!this.current_rule.hash.validations.day_of_month) { this.current_rule.hash.validations.day_of_month = []; }
143+
if (!this.current_rule.hash.validations.day_of_week) { this.current_rule.hash.validations.day_of_week = {}; }
140144
this.init_calendar_days(section);
141145
this.init_calendar_weeks(section);
142146

@@ -156,6 +160,40 @@ class RecurringSelectDialog {
156160
section.style.display = 'block'
157161
}
158162

163+
terminationInit() {
164+
const section = this.outer_holder.querySelector(".rs_termination_section");
165+
this.until_date = section.querySelector("#rs_until_date");
166+
this.until_date.flatpickr({
167+
enableTime: true,
168+
dateFormat: "Y-m-d H:i",
169+
altInput: true,
170+
altFormat: "F j, Y h:i K",
171+
});
172+
173+
if (this.current_rule.hash && this.current_rule.hash.count) {
174+
this.count_option = section.querySelector("input[name=rs_termination][value=count]");
175+
this.count_option.checked = true;
176+
this.occurence_count = section.querySelector("#rs_occurrence_count");
177+
this.occurence_count.value = this.current_rule.hash.count;
178+
} else if (this.current_rule.hash && this.current_rule.hash.until) {
179+
this.until_option = section.querySelector("input[name=rs_termination][value=until]");
180+
this.until_option.checked = true;
181+
// IceCube::TimeUtil will serialize a TimeWithZone into a hash, such as:
182+
// {time: Thu, 04 Sep 2014 06:59:59 +0000, zone: "Pacific Time (US & Canada)"}
183+
// If we're initializing from an unsaved rule, until will be a string
184+
if (this.current_rule.hash.until.time) {
185+
this.until_val = new Date(this.current_rule.hash.until.time);
186+
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());
187+
} else {
188+
this.until_date.value = this.current_rule.hash.until;
189+
}
190+
} else {
191+
this.never_option = section.querySelector("input[name=rs_termination][value=never]");
192+
this.never_option.checked = true;
193+
}
194+
195+
section.addEventListener("change", this.terminationChanged.bind(this));
196+
}
159197

160198
summaryInit() {
161199
this.summary = this.outer_holder.querySelector(".rs_summary");
@@ -212,7 +250,7 @@ class RecurringSelectDialog {
212250
if (Array.from(this.current_rule.hash.validations.day_of_month).includes(num)) {
213251
day_link.classList.add("selected");
214252
}
215-
};
253+
}
216254

217255
// add last day of month button
218256
const end_of_month_link = document.createElement("a")
@@ -248,9 +286,9 @@ class RecurringSelectDialog {
248286
day_link.setAttribute("day", day_of_week);
249287
day_link.setAttribute("instance", num);
250288
monthly_calendar.appendChild(day_link);
251-
};
289+
}
252290
}
253-
};
291+
}
254292

255293
Object.entries(this.current_rule.hash.validations.day_of_week).forEach(([key, value]) => {
256294
Array.from(value).forEach((instance, index) => {
@@ -278,10 +316,9 @@ class RecurringSelectDialog {
278316
freqChanged() {
279317
if (!isPlainObject(this.current_rule.hash)) { this.current_rule.hash = null; } // for custom values
280318

281-
if (!this.current_rule.hash) { this.current_rule.hash = {} };
319+
if (!this.current_rule.hash) { this.current_rule.hash = {} }
320+
this.current_rule.str = null;
282321
this.current_rule.hash.interval = 1;
283-
this.current_rule.hash.until = null;
284-
this.current_rule.hash.count = null;
285322
this.current_rule.hash.validations = null;
286323
this.content.querySelectorAll(".freq_option_section").forEach(el => el.style.display = 'none')
287324
this.content.querySelector("input[type=radio], input[type=checkbox]").checked = false
@@ -305,13 +342,13 @@ class RecurringSelectDialog {
305342
this.current_rule.hash.rule_type = "IceCube::DailyRule";
306343
this.current_rule.str = this.config.texts["daily"];
307344
this.initDailyOptions();
308-
};
309-
this.summaryUpdate();
345+
}
346+
this.summaryFetch();
310347
}
311348

312349
intervalChanged(event) {
313350
this.current_rule.str = null;
314-
if (!this.current_rule.hash) { this.current_rule.hash = {} };
351+
if (!this.current_rule.hash) { this.current_rule.hash = {}; }
315352
this.current_rule.hash.interval = parseInt(event.currentTarget.value);
316353
if ((this.current_rule.hash.interval < 1) || isNaN(this.current_rule.hash.interval)) {
317354
this.current_rule.hash.interval = 1;
@@ -322,7 +359,7 @@ class RecurringSelectDialog {
322359
daysChanged(event) {
323360
event.target.classList.toggle("selected");
324361
this.current_rule.str = null;
325-
if (!this.current_rule.hash) { this.current_rule.hash = {} };
362+
if (!this.current_rule.hash) { this.current_rule.hash = {}; }
326363
this.current_rule.hash.validations = {};
327364
const raw_days = Array.from(this.content.querySelectorAll(".day_holder a.selected"))
328365
.map(el => parseInt(el.dataset.value))
@@ -334,7 +371,7 @@ class RecurringSelectDialog {
334371
dateOfMonthChanged(event) {
335372
event.target.classList.toggle("selected");
336373
this.current_rule.str = null;
337-
if (!this.current_rule.hash) { this.current_rule.hash = {} };
374+
if (!this.current_rule.hash) { this.current_rule.hash = {}; }
338375
this.current_rule.hash.validations = {};
339376
const raw_days = Array.from(this.content.querySelectorAll(".monthly_options .rs_calendar_day a.selected"))
340377
.map(el => {
@@ -349,21 +386,44 @@ class RecurringSelectDialog {
349386
weekOfMonthChanged(event) {
350387
event.target.classList.toggle("selected");
351388
this.current_rule.str = null;
352-
if (!this.current_rule.hash) { this.current_rule.hash = {} };
389+
if (!this.current_rule.hash) { this.current_rule.hash = {}; }
353390
this.current_rule.hash.validations = {};
354391
this.current_rule.hash.validations.day_of_month = [];
355392
this.current_rule.hash.validations.day_of_week = {};
356393
this.content.querySelectorAll(".monthly_options .rs_calendar_week a.selected")
357394
.forEach((elm, index) => {
358395
const day = parseInt(elm.getAttribute("day"));
359396
const instance = parseInt(elm.getAttribute("instance"));
360-
if (!this.current_rule.hash.validations.day_of_week[day]) { this.current_rule.hash.validations.day_of_week[day] = [] };
397+
if (!this.current_rule.hash.validations.day_of_week[day]) { this.current_rule.hash.validations.day_of_week[day] = []; }
361398
return this.current_rule.hash.validations.day_of_week[day].push(instance);
362399
})
363400
this.summaryUpdate();
364401
return false;
365402
}
366403

404+
terminationChanged() {
405+
this.selected_termination_type = this.outer_holder.querySelector(".rs_termination_section input[type='radio']:checked");
406+
if (!this.selected_termination_type) { return; }
407+
this.current_rule.str = null;
408+
if (!this.current_rule.hash) { this.current_rule.hash = {}; }
409+
switch (this.selected_termination_type.value) {
410+
case "count":
411+
this.current_rule.hash.count = parseInt(this.occurence_count ? this.occurence_count.value : this.outer_holder.querySelector("#rs_occurrence_count").value);
412+
if ((this.current_rule.hash.count < 1) || isNaN(this.current_rule.hash.count)) {
413+
this.current_rule.hash.count = 1;
414+
}
415+
this.current_rule.hash.until = null;
416+
break
417+
case "until":
418+
this.current_rule.hash.until = this.until_date ? this.until_date.value : this.outer_holder.querySelector("#rs_until_date").value;
419+
this.current_rule.hash.count = null;
420+
break
421+
default:
422+
this.current_rule.hash.count = null;
423+
this.current_rule.hash.until = null;
424+
}
425+
this.summaryUpdate();
426+
}
367427
// ========================= Change callbacks ===============================
368428

369429
template() {
@@ -381,7 +441,6 @@ class RecurringSelectDialog {
381441
<option value='Yearly'>${this.config.texts["yearly"]}</option> \
382442
</select> \
383443
</p> \
384-
\
385444
<div class='daily_options freq_option_section'> \
386445
<p> \
387446
${this.config.texts["every"]} \
@@ -400,7 +459,7 @@ class RecurringSelectDialog {
400459
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) {
401460
day_of_week = day_of_week % 7;
402461
str += `<a href='#' data-value='${day_of_week}'>${this.config.texts["days_first_letter"][day_of_week]}</a>`;
403-
};
462+
}
404463

405464
str += `\
406465
</div> \
@@ -426,6 +485,31 @@ class RecurringSelectDialog {
426485
${this.config.texts["years"]} \
427486
</p> \
428487
</div> \
488+
<div class='rs_termination_section'>
489+
<table>
490+
<tr>
491+
<td>
492+
<label class='rs_termination_label'>${this.config.texts["ends"]}:</label>
493+
</td>
494+
<td>
495+
<label>
496+
<input type='radio' name='rs_termination' value='never' />
497+
${this.config.texts["never"]}
498+
</label><br>
499+
<label>
500+
<input type='radio' name='rs_termination' value='count' />
501+
${this.config.texts["after"]}
502+
<input type='text' data-wrapper-class='ui-recurring-select' id='rs_occurrence_count' class='rs_count' value='10' size='2' />
503+
${this.config.texts["occurrences"]}
504+
</label><br>
505+
<label>
506+
<input type='radio' name='rs_termination' value='until' />
507+
${this.config.texts["on"]}
508+
<input type='text' data-wrapper-class='ui-recurring-select' class='rs_datepicker' id='rs_until_date' />
509+
</label>
510+
</td>
511+
</table>
512+
</div>
429513
<p class='rs_summary'> \
430514
<span></span> \
431515
</p> \

app/assets/stylesheets/recurring_select.scss

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
@import "utilities.scss";
2-
32
/* -------- resets ---------------*/
43

54
.rs_dialog_holder {
@@ -98,6 +97,23 @@ select {
9897
}
9998
}
10099

100+
.rs_termination_section {
101+
table {
102+
margin: 0;
103+
padding-top: 5px;
104+
td {
105+
padding: 0;
106+
vertical-align: top;
107+
}
108+
}
109+
.rs_termination_label {margin-right:10px;}
110+
.rs_count {width:30px; text-align:center; display: inline-block;}
111+
}
112+
113+
.rs_datepicker {
114+
width: 150px;
115+
text-align: center;
116+
}
101117

102118
.rs_summary {
103119
padding: 0px;
@@ -128,4 +144,4 @@ select {
128144
}
129145

130146
}
131-
}
147+
}

lib/recurring_select.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ def self.filter_params(params)
4444

4545
params[:interval] = params[:interval].to_i if params[:interval]
4646
params[:week_start] = params[:week_start].to_i if params[:week_start]
47+
params[:count] = params[:count].to_i if params[:count]
4748

4849
params[:validations] ||= {}
4950
params[:validations].symbolize_keys!
@@ -77,6 +78,23 @@ def self.filter_params(params)
7778
params[:validations][:day_of_year] = params[:validations][:day_of_year].collect(&:to_i)
7879
end
7980

81+
begin
82+
# IceCube::TimeUtil will serialize a TimeWithZone into a hash, such as:
83+
# {time: Thu, 04 Sep 2014 06:59:59 +0000, zone: "Pacific Time (US & Canada)"}
84+
# So don't try to DateTime.parse the hash. IceCube::TimeUtil will deserialize this for us.
85+
if (until_param = params[:until])
86+
if until_param.is_a?(String)
87+
# Set to 23:59:59 (in current TZ) to encompass all events on until day
88+
params[:until] = Time.zone.parse(until_param).change(hour: 23, min: 59, sec: 59)
89+
elsif until_param.is_a?(Hash) # ex: {time: Thu, 28 Aug 2014 06:59:590000, zone: "Pacific Time (US & Canada)"}
90+
until_param = until_param.symbolize_keys
91+
params[:until] = until_param[:time].in_time_zone(until_param[:zone])
92+
end
93+
end
94+
rescue ArgumentError
95+
# Invalid date given, attempt to assign :until will fail silently
96+
end
97+
8098
params
8199
end
82100
end

lib/recurring_select/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module RecurringSelect
2-
VERSION = "4.0.0"
2+
VERSION = "4.0.1"
33
end

recurring_select.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Gem::Specification.new do |s|
1717
s.add_dependency "rails", ">= 6.1"
1818
s.add_dependency "ice_cube", ">= 0.11"
1919
s.add_dependency "sass-rails", ">= 6.0"
20+
s.add_dependency "flatpickr", ">= 4.6.6"
2021

2122
s.add_development_dependency "bundler", ">= 1.3.5"
2223
s.add_development_dependency "rspec-rails", ">= 2.14"

spec/dummy/app/assets/javascripts/application.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
55
// the compiled file.
66
//
7+
//= require flatpickr/dist/flatpickr.min.js
78
//= require recurring_select
89
//= require_tree .
9-
1010
RecurringSelectDialog.config.options.monthly = {
1111
show_week: [true, true, true, true, true, true]
1212
}

spec/dummy/app/assets/stylesheets/application.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* the top of the compiled file, but it's generally better to create a new file per style scope.
55
*= require_self
66
*= require recurring_select
7+
*= require flatpickr
78
*/
89

910

spec/dummy/config/application.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ class Application < Rails::Application
4444

4545
# Version of your assets, change this if you want to expire all your assets
4646
config.assets.version = '1.0'
47+
48+
config.assets.paths << Rails.root.join('../../node_modules')
4749
end
4850
end
4951

0 commit comments

Comments
 (0)