date_recur_interactive-8.x-2.0-alpha1/js/date_recur_rrule.widget.js
js/date_recur_rrule.widget.js
/*
* A JQuery UI Widget to create a RRule compatible inputs for use inside a form
* Requires: rrule.js (http://jkbr.github.io/rrule/)
* underscore.js
* Original author: Josh Levinger, 2013.
* Original source: https://github.com/rootio/rootio_web/blob/master/rootio/static/js/plugins/rrule.recurringinput.js
* Original license: AGPL3.
* Relicensed by Josh Levinger to GPL v2+.
* Modified and adapted to Drupal 8 by Frando, 2016.
*/
(function ($, Drupal, Modernizr, RRule) {
var widget_count = 0;
RRule.FREQUENCY_ADVERBS = [
Drupal.t('yearly', {}, {context: 'Date recur: Freq'}),
Drupal.t('monthly', {}, {context: 'Date recur: Freq'}),
Drupal.t('weekly', {}, {context: 'Date recur: Freq'}),
Drupal.t('daily', {}, {context: 'Date recur: Freq'}),
Drupal.t('hourly', {}, {context: 'Date recur: Freq'}),
Drupal.t('minutely', {}, {context: 'Date recur: Freq'}),
Drupal.t('secondly', {}, {context: 'Date recur: Freq'})
];
// add helpful constants to RRule
RRule.FREQUENCY_NAMES = [
Drupal.t('year'),
Drupal.t('month'),
Drupal.t('week'),
Drupal.t('day'),
Drupal.t('hour'),
Drupal.t('minute'),
Drupal.t('second')
];
RRule.FREQUENCY_NAMES_PLURAL = [
Drupal.t('years'),
Drupal.t('months'),
Drupal.t('weeks'),
Drupal.t('days'),
Drupal.t('hours'),
Drupal.t('minutes'),
Drupal.t('seconds')
];
RRule.DAYCODES = ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'];
RRule.DAYNAMES = [
Drupal.t('Monday'),
Drupal.t('Tuesday'),
Drupal.t('Wednesday'),
Drupal.t('Thursday'),
Drupal.t('Friday'),
Drupal.t('Saturday'),
Drupal.t('Sunday')
];
RRule.MONTHS = [
Drupal.t('Jan'),
Drupal.t('Feb'),
Drupal.t('Mar'),
Drupal.t('Apr'),
Drupal.t('May'),
Drupal.t('Jun'),
Drupal.t('Jul'),
Drupal.t('Aug'),
Drupal.t('Sep'),
Drupal.t('Oct'),
Drupal.t('Nov'),
Drupal.t('Dec')
];
RRule.SETPOS = {
'1': Drupal.t('first', {}, {context: 'Date recur: Freq'}),
'2': Drupal.t('second', {}, {context: 'Date recur: Freq'}),
'3': Drupal.t('third', {}, {context: 'Date recur: Freq'}),
'4': Drupal.t('forth', {}, {context: 'Date recur: Freq'}),
'5': Drupal.t('fifth', {}, {context: 'Date recur: Freq'}),
'-1': Drupal.t('last', {}, {context: 'Date recur: Freq'}),
};
// @todo: localize. @see: locale.datepicker.js.
// note, month num for these values should be one-based, not zero-based
$.widget("rrule.recurringinput", {
// default options
options: {
rrule: '',
dtstart: null
},
_create: function () {
this._exinc = {};
this._exinc.exclude = [];
this._exinc.include = [];
//set up inputs
var tmpl = '';
//frequency
tmpl += '<div class="container-inline">';
tmpl += '<label class="controls">'
+ Drupal.t('Repeat', {}, {context: 'Date recur'})
+ ' ';
tmpl += '<select name="freq">';
_.each(RRule.FREQUENCIES, function (element, index) {
var f = RRule[element];
tmpl += '<option value=' + f + ' data-freq-base="' + element.toLowerCase() + '">' + RRule.FREQUENCY_ADVERBS[index] + '</option>';
});
tmpl += '</select>';
tmpl += '</label>';
tmpl += '<label class="controls"> ';
tmpl += Drupal.t('every', {}, {context: 'Date recur'})
tmpl += ' <input type="number" value="1" min="1" max="100" name="interval"/>';
tmpl += ' <span id="frequency_name"></span>';
tmpl += '</label>';
tmpl += '</div>';
// repeat options, frequency specific
// data-freq should be lowercase value from FREQUENCY_NAMES
//bymonth: weekdays
tmpl += '<div class="repeat-options controls container-inline" data-freq="monthly">';
tmpl += '<label for="byweekday-pos">';
tmpl += Drupal.t('On the', {}, {context: 'Date recur: weekday in month'})
tmpl += '</label>';
tmpl += ' <div class="byweekday-pos-container"><span class="byweekday-pos-text"></span>';
tmpl += ' <div class="byweekday-pos-input">';
_.each(RRule.SETPOS, function (element, index) {
tmpl += '<label><input type="checkbox" name="byweekday-pos" value=' + index + '> ' + element + '</label>';
});
tmpl += '</div>';
tmpl += '</div>';
_.each(RRule.DAYCODES, function (element, index) {
var d = RRule[element];
tmpl += '<label class="inline">';
tmpl += '<input type="checkbox" name="byweekday" value="' + d.weekday + '" />';
tmpl += RRule.DAYNAMES[index] + '</label>';
});
tmpl += '</div>';
//bymonth: months
tmpl += '<div class="repeat-options controls container-inline" data-freq="monthly">';
tmpl += Drupal.t('Only in', {}, {context: 'Date recur: month'})
tmpl += ' </label>';
_.each(RRule.MONTHS, function (element, index) {
tmpl += '<label class="inline">';
tmpl += '<input type="checkbox" name="bymonth" value="' + (index + 1) + '" />';
tmpl += element + '</label>';
});
tmpl += '</div>';
//byweekday
tmpl += '<div class="repeat-options controls container-inline" data-freq="weekly">';
tmpl += '<label for="byweekday">';
tmpl += Drupal.t('On', {}, {context: 'Date recur: weekday'})
tmpl += ' </label>';
_.each(RRule.DAYCODES, function (element, index) {
var d = RRule[element];
tmpl += '<label class="inline">';
tmpl += '<input type="checkbox" name="byweekday" value="' + d.weekday + '" />';
tmpl += RRule.DAYNAMES[index] + '</label>';
});
tmpl += '</div>';
//byhour
tmpl += '<label class="repeat-options" data-freq="hourly">';
tmpl += Drupal.t('Only at', {}, {context: 'Date recur: time'})
tmpl += ' <input name="byhour" /> <span>o\'clock</span></label>';
//byminute
tmpl += '<label class="repeat-options" data-freq="minutely">';
tmpl += Drupal.t('Only at', {}, {context: 'Date recur: time'})
tmpl += ' <input name="byminute" /> <span>minutes<span></label>';
//bysecond
tmpl += '<label class="repeat-options" data-freq="secondly">';
tmpl += Drupal.t('Only at', {}, {context: 'Date recur: time'})
tmpl += ' <input name="bysecond" /> <span>seconds</span></label>';
// end repeat options
// end on
this.end_input_name = 'end-' + widget_count;
tmpl += '<div class="end-options controls">';
tmpl += '<label for="' + this.end_input_name + '">';
tmpl += Drupal.t('End', {}, {context: 'Date recur'})
tmpl += ' </label>';
tmpl += '<label class="inline">';
tmpl += '<input type="radio" name="' + this.end_input_name + '" value="0" checked="checked" class="end-radio"/> ';
tmpl += Drupal.t('Never', {}, {context: 'Date recur'})
tmpl += '</label>';
tmpl += '<label class="inline">';
tmpl += '<input type="radio" name="' + this.end_input_name + '" value="1" class="end-radio" /> ';
tmpl += Drupal.t('After !count occurrences', {'!count': '<input type="number" max="1000" min="1" value="" name="count"/> '}, {context: 'Date recur'})
tmpl += '</label>';
tmpl += '<label class="inline">';
tmpl += '<input type="radio" name="' + this.end_input_name + '" value="2" class="end-radio"> ';
tmpl += Drupal.t('On date !date', {'!date': '<input type="date" name="until"/>'}, {context: 'Date recur'})
tmpl += '</label>';
tmpl += '</div>';
// exclude/include
tmpl += '<div class="exlude-include-options controls">';
tmpl += '<label for="exclude-include">';
tmpl += Drupal.t('Exclude/include dates', {}, {context: 'Date recur'});
tmpl += ' </label>';
tmpl += '<label class="inline">';
tmpl += '<select name="exinc-type">';
tmpl += '<option value="exclude">' + Drupal.t('Exclude') + '</option>';
tmpl += '<option value="include">' + Drupal.t('Include') + '</option>';
tmpl += '</select>';
tmpl += '<input type="date" name="exinc-date" />';
tmpl += '<input type="button" name="exinc-add" value="Add" />';
tmpl += '</label>';
tmpl += '<div class="exinc-dates-container"></div>'
tmpl += '</div>';
// summary
tmpl += '<label for="output">';
tmpl += Drupal.t('Summary', {}, {context: 'Date recur'})
tmpl += ': <em class="text-output"></em></label>'; // human readable
tmpl += '<label>' + Drupal.t('RRule') + ':<code class="rrule-output"></code></label>'; // ugly rrule
//TODO: show next few instances to help user debug
//render template
this.element.append(tmpl);
// attach datepicker if needed.
if (Modernizr.inputtypes.date !== true) {
this.element.find("input[type=date]").once('datePicker').each(function() {
var datepickerSettings = {
dateFormat: 'yy-mm-dd'
};
$(this).datepicker(datepickerSettings);
})
}
//save input references to widget for later use
this.frequency_select = this.element.find('select[name="freq"]');
this.interval_input = this.element.find('input[name="interval"]');
this.end_input = this.element.find('input[type="radio"][name="' + this.end_input_name + '"]');
this.byweekday_pos_input = this.element.find('.byweekday-pos-input');
//bind event handlers
this._on(this.element.find('select, input'), {
change: this._refresh
});
this._on(this.element.find('input[name=exinc-add]'), {
"click": this._addExcludeInclude
});
// Handle byweekday_pos popup.
this.byweekday_pos_input.hide();
var widget = this;
this.element.find('.byweekday-pos-text').click(function(e) {
if ($(this).hasClass('select-shown')) {
$(this).removeClass('select-shown');
widget.byweekday_pos_input.hide();
}
else {
$(this).addClass('select-shown');
widget.byweekday_pos_input.show();
}
e.stopPropagation();
});
this.byweekday_pos_input.click(function(e) {
e.stopPropagation();
});
$(document).click(function(e) {
if (widget.byweekday_pos_input.is(':visible')) {
widget.byweekday_pos_input.hide();
widget.element.find('.byweekday-pos-text')
}
});
//set sensible defaults
this.frequency_select.val(2);
this.interval_input.val(1);
// setup default.
var rrule;
var rruleOpts = {};
if (this.options.dtstart) {
rruleOpts.dtstart = this.options.dtstart;
}
if (this.options.rrule.length) {
rrule = RRule.rrulestr(this.options.rrule, {forceset: true});
rruleOpts = rrule._rrule[0].options;
}
else {
rruleOpts.freq = RRule.WEEKLY;
if (this.options.dtstart) {
rruleOpts.byweekday = [6, 0, 1, 2, 3, 4, 5][this.options.dtstart.getDay()];
}
}
if (typeof rrule == 'undefined') {
rrule = new RRule.RRuleSet();
rrule.rrule(new RRule(rruleOpts));
}
try {
this._applyRRule(rrule);
} catch (_error) {
e = _error;
$(".text-output", this.element).append($('<pre class="error"/>').text('=> ' + String(e || null)));
return;
}
widget_count++;
//refresh
this._refresh();
},
_applyRRule: function (rrule) {
var opts = rrule._rrule[0].options;
var freq = RRule.FREQUENCY_NAMES[opts.freq].toLowerCase();
// split byweekday.
var byweekdayPos = [];
if (opts.byweekday == null) {
opts.byweekday = [];
}
if (opts.bynweekday instanceof Array) {
_.each(opts.bynweekday, function (el) {
opts.byweekday.push(el[0]);
byweekdayPos.push(el[1]);
});
}
byweekdayPos.sort(function (a, b) {
if (a === -1) {
return 1;
}
return a - b;
});
opts['byweekday-pos'] = byweekdayPos;
var $sel = $('[data-freq!=' + freq + ']', this.element);
var k;
for (k in opts) {
var v = opts[k];
// Try to set the value.
if (v instanceof Array) {
$('input[name=' + k + '][type=checkbox]', $sel).val(v);
if ($('select[name=' + k + ']', $sel).length) {
$.each(v, function(i, e) {
$('select[name=' + k + '] option[value=' + e + ']', $sel).prop("selected", true);
})
}
}
else {
if ($('input[name=' + k + '][type!=checkbox]', $sel).val(v).length) {
}
else if ($('select[name=' + k + ']', $sel).val(v).length) {
}
}
}
if (opts.count) {
$('input[name=count]', this.element).val(opts.count);
$('input[name="' + this.end_input_name + '"][value=1]', this.element).prop('checked', true);
}
if (opts.until) {
$('input[name=until]', this.element)[0].valueAsDate = opts.until;
$('input[name="' + this.end_input_name + '"][value=2]', this.element).prop('checked', true);
}
for (key in rrule._rdate) {
this._exinc.include.push(rrule._rdate[key]);
}
for (key in rrule._exdate) {
this._exinc.exclude.push(rrule._exdate[key]);
}
},
_createWeekdayPosString: function($boxes) {
var names = [];
$boxes.each(function() {
if ($(this).prop('checked')) {
names.push(RRule.SETPOS[$(this).attr('value')]);
}
});
if (names.length) {
return names.join(', ');
}
else {
return Drupal.t('(select a week)');
}
},
_addExcludeInclude: function() {
var date = this.element.find('input[name=exinc-date]').val();
if (date) {
var type = this.element.find('select[name=exinc-type]').val();
this._exinc[type].push(date);
this._refresh();
}
},
// called on create and when changing options
_refresh: function () {
var that = this;
//determine selected frequency
var frequency = this.frequency_select.find("option:selected");
var frequency_text = frequency.attr('data-freq-base');
// fill in frequency-name span
this.element.find('#frequency_name').text(RRule.FREQUENCY_NAMES[frequency.val()]);
// and pluralize
if (this.interval_input.val() > 1) {
this.element.find('#frequency_name').text(RRule.FREQUENCY_NAMES_PLURAL[frequency.val()]);
}
this.element.find('.byweekday-pos-text').text(this._createWeekdayPosString(this.element.find('input[name=byweekday-pos]')));
// display appropriate repeat options
var repeatOptions = this.element.find('.repeat-options');
repeatOptions.hide();
if (frequency !== "") {
//show options for the selected frequency
repeatOptions.filter('[data-freq=' + frequency_text + ']').show();
//and clear descendent fields for the others
var nonSelectedOptions = repeatOptions.filter('[data-freq!=' + frequency_text + ']');
nonSelectedOptions.find('input[type=checkbox]:checked').removeAttr('checked');
nonSelectedOptions.find('input[type!=checkbox]').val('');
nonSelectedOptions.find('select').val('');
}
//reset end
switch (this.end_input.filter(':checked').val()) {
case "0":
//never, clear count and until
this.end_input.siblings('input[name=count]').val('');
this.end_input.siblings('input[name=until]').val('');
break;
case "1":
//after, clear until
this.end_input.siblings('input[name=until]').val('');
break;
case "2":
//date, clear count
this.end_input.siblings('input[name=count]').val('');
break;
}
// set up exclude/include
var exincTpl = {};
var exincStr = '';
var type, dateKey;
for (type in this._exinc) {
exincTpl[type] = [];
for (dateKey in this._exinc[type]) {
var date = this._exinc[type][dateKey];
exincTpl[type].push(
'<span class="excinc-date-val" data-exinc-date="' + date + '" data-exinc-type="' + type + '">'
+ this._formatDate(this._parseExincDate(date))
+ ' <span class="exinc-date-remove">✘</span>'
+ '</span>'
);
}
}
if (exincTpl.exclude.length) {
exincStr += '<div class="container-inline"><label>' + Drupal.t('Exclude:') + '</label> ' + exincTpl.exclude.join(', ') + '</div>';
}
if (exincTpl.include.length) {
exincStr += '<div class="container-inline"><label>' + Drupal.t('Include:') + '</label> ' + exincTpl.include.join(', ') + '</div>';
}
this.element.find('.exinc-dates-container').html(exincStr);
this.element.find('.exinc-date-remove').click(function() {
var parent = $(this).parent()[0];
var type = $(parent).data('exinc-type');
var date = $(parent).data('exinc-date');
that._exinc[type] = _.without(that._exinc[type], _.findWhere(that._exinc[type], date));
that._refresh();
});
//determine rrule
var rrule = this._getRRule();
if (rrule) {
$('.rrule-output', this.element).text(rrule.valueOf().join("\n"));
$('.text-output', this.element).text(this._getHumanReadable(rrule));
this.element.trigger('rrule-update');
}
},
_getHumanReadable: function(rrule) {
var that = this;
var text = '';
if (rrule._rrule[0]) {
text = text + rrule._rrule[0].toText();
}
if (rrule._rdate.length) {
text = Drupal.t('!text, and also on: !dates', {
'!text': text,
'!dates': _.map(rrule._rdate, function(el) {
return that._formatDate(el);
}).join(', ')
});
}
if (rrule._exdate.length) {
text = Drupal.t('!text, but not on: !dates', {
'!text': text,
'!dates': _.map(rrule._exdate, function(el) {
return that._formatDate(el);
}).join(', ')
});
}
return text;
},
_getFormValues: function ($form) {
//modified from rrule/tests/demo/demo.js
var paramObj;
paramObj = {};
$.each($form.serializeArray(), function (_, kv) {
if (paramObj.hasOwnProperty(kv.name)) {
paramObj[kv.name] = $.makeArray(paramObj[kv.name]);
return paramObj[kv.name].push(kv.value);
} else {
return paramObj[kv.name] = kv.value;
}
});
return paramObj;
},
_getRRule: function () {
//modified from rrule/tests/demo/demo.js
//ignore 'end', because it's part of the ui but not the spec
var values = this._getFormValues($(this.element).find('select, input[class!="end-radio"]'));
var options = {};
if (_.has(values, 'byweekday-pos') && _.has(values, 'byweekday')) {
var weekdayPos = values['byweekday-pos'];
}
delete values['byweekday-pos'];
delete values['exinc-type'];
delete values['exinc-date'];
var getDay = function (i) {
var days = [RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR, RRule.SA, RRule.SU];
if (typeof weekdayPos !== 'undefined') {
if (weekdayPos instanceof Array) {
return _.map(weekdayPos, function (pos) {
return days[i].nth(pos)
});
}else {
return days[i].nth(weekdayPos);
}
}
return [days[i]];
};
var k, v;
for (k in values) {
v = values[k];
if (!v) {
continue;
}
if (_.contains(["dtstart", "until"], k)) {
v = this._parseDate(v);
} else if (k === 'byweekday') {
if (v instanceof Array) {
v = _.flatten(_.map(v, getDay), true);
} else {
v = getDay(v);
}
} else if (/^by/.test(k)) {
if (!(v instanceof Array)) {
v = _.compact(v.split(/[,\s]+/));
}
v = _.map(v, function (n) {
return parseInt(n, 10);
});
} else {
v = parseInt(v, 10);
}
if (k === 'wkst') {
v = getDay(v);
}
if (k === 'interval' && v === 1) {
continue;
}
options[k] = v;
}
// get exclude/include dates.
var dates = {include: [], exclude: []};
var that = this;
this.element.find('[data-exinc-date]').each(function() {
var type = $(this).data('exinc-type');
dates[type].push(that._parseExincDate($(this).data('exinc-date')));
});
var rule, type, key;
try {
rule = new RRule.RRuleSet();
rule.rrule(new RRule(options));
for (type in dates) {
for (key in dates[type]) {
if (type == 'include') {
rule.rdate(dates[type][key]);
}
if (type == 'exclude') {
rule.exdate(dates[type][key]);
}
}
}
} catch (_error) {
var e = _error;
$(".text-output", this.element).append($('<pre class="error"/>').text('=> ' + String(e || null)));
return;
}
return rule;
},
// _setOptions is called with a hash of all options that are changing
// always refresh when changing options
_setOptions: function () {
this._superApply(arguments);
this._refresh();
},
_parseDate: function(dateval) {
var d = new Date(Date.parse(dateval));
return new Date(d.getTime() + (d.getTimezoneOffset() * 60 * 1000));
},
_parseExincDate: function(dateval) {
var d = new Date(Date.parse(dateval));
return d;
},
_formatDate: function(dateobj) {
return Drupal.t('!month/!day/!year', {
'!day': this._pad(dateobj.getDate()),
'!month': this._pad(dateobj.getMonth() + 1),
'!year': this._pad(dateobj.getFullYear())
}, {context: 'Date recur'});
},
_pad: function(n) {
return (n < 10) ? ("0" + n) : n;
},
destroy: function () {
// remove references
this.frequency_select.remove();
this.interval_input.remove();
// unbind events
// clear templated html
this.element.html("");
$.Widget.prototype.destroy.apply(this);
}
});
}(jQuery, Drupal, Modernizr, RRule));
