Option name | Type | Description |
---|---|---|
el | Element | |
params | Object |
DateInput constructor.
var DateInput = function(el, params) {
if (!el) {
return;
}
this._setParams(this.defaults, true);
this._cacheElements(el);
this._bindEventListenerCallbacks();
this._setParams(params || {});
this._parseParams();
this._convertLabel();
this._initializeInputs();
this._updateClass();
this._addEventListeners();
};
DateInput.prototype = {
Include common functionality.
_setParams: Base.setParams,
_toggleClass: Base.toggleClass,
_removeClass: Base.removeClass,
_addClass: Base.addClass,
_hasClass: Base.hasClass,
_getChildIndex: Base.getChildIndex,
_appendChildren: Base.appendChildren,
_triggerEvent: Base.triggerEvent,
_copyAttributes: Base.copyAttributes,
_getElementMatchingParent: Base.getElementMatchingParent,
Inherit functionality from TextInput.
setError: TextInput.prototype.setError,
clearError: TextInput.prototype.clearError,
setWarning: TextInput.prototype.setWarning,
clearWarning: TextInput.prototype.clearWarning,
setSuccess: TextInput.prototype.setSuccess,
clearSuccess: TextInput.prototype.clearSuccess,
clearMessages: TextInput.prototype.clearMessages,
setMessage: TextInput.prototype.setMessage,
_showMessage: TextInput.prototype._showMessage,
_hideMessage: TextInput.prototype._hideMessage,
_isMessageVisible: TextInput.prototype._isMessageVisible,
Whitelisted parameters which can be set on construction.
_whitelistedParams: ['onChange', 'onFocus', 'onBlur', 'isTypeahead', 'isSelect', 'format', 'textFormat', 'showDateAsText'],
Default values for internal properties we will be setting.
These are set on each construction so we don't leak properties
into the prototype chain.
defaults: {
el: null,
inputEl: null,
messageEl: null,
toggleEl: null,
inFocus: null,
isActive: null,
isSelect: null,
isTypeahead: null,
typeaheads: null,
typeaheadEls: null,
selects: null,
selectEls: null,
format: null,
parsedFormat: null,
showDateAsText: null,
runningTypeaheads: false,
textFormat: null,
onChange: null,
onFocus: null,
onBlur: null,
_hasFocus: false,
_pauseInputChange: false,
_onClickBound: null,
_onPieceChangeBound: null,
_onTypeaheadFocusBound: null,
_onTypeaheadBlurBound: null,
_onTypeaheadBackspaceBound: null,
_onTypeaheadEndBound: null,
_onInputChangeBound: null
},
Show the input by adding the active state and setting character counts (if necessary).
show: function() {
if (!this.isActive) {
this._runTypeaheads();
this.isActive = true;
this._updateClass();
}
},
Hide the input by removing the active state.
hide: function() {
this.isActive = false;
this._updateClass();
},
Augment default remove call w/ helper cleanup.
remove: function() {
Base.remove.apply(this, arguments);
// Remove text input
this.textInput.remove();
delete this.textInput;
// Remove typeaheads
if (this.typeaheads) {
for (var i in this.typeaheads) {
this.typeaheads[i].remove();
}
}
// Remove select inputs
if (this.selectEls) {
this.selectEls.forEach(this._removeSelectPiece.bind(this));
}
},
Option name | Type | Description |
---|---|---|
values | Object |
Given an object with day, month and year, set the value of the input.
setValue: function(values) {
values = values || {
day: '',
month: '',
year: ''
};
var i;
var hadValue;
for (i in this.typeaheads) {
if (values[i] !== undefined) {
this.typeaheads[i].setValue(values[i]);
hadValue = hadValue || (values[i] ? true : false);
}
}
for (i in this.selects) {
if (values[i] !== undefined) {
this.selects[i].setValue(values[i]);
hadValue = hadValue || (values[i] ? true : false);
}
}
if (!this.isActive && hadValue) {
this.isActive = true;
}
this._padTypeaheads();
this._updateClass();
this.updateInput();
},
Validate the date values.
validate: function() {
if (this.isTypeahead) {
this._validateTypeaheads();
}
else if (this.isSelect) {
this._validateSelects();
}
},
Update the input values to match the typeaheads.
updateInput: function() {
var inputs;
if (this.isTypeahead && this.typeaheads) {
inputs = this.typeaheads;
}
else if (this.isSelect) {
inputs = this.selects;
}
if (inputs) {
var day = (inputs.day && inputs.day.getValue(true)) || 0;
var month = (inputs.month && inputs.month.getValue(true)) || 0;
var year = (inputs.year && inputs.year.getValue(true)) || 0;
var val = this.inputEl.value;
this.inputEl.value = [day, month, year].indexOf(0) === -1 ? padNumber(year, 4) + '-' + padNumber(month, 2) + '-' + padNumber(day, 2) : '';
if (val !== this.inputEl.value) {
this._pauseInputChange = true;
this._triggerEvent(this.inputEl, 'change');
(this.onChange || noop)(this.inputEl.value, this.inputEl);
this._pauseInputChange = false;
}
}
},
Option name | Type | Description |
---|---|---|
i | Number | |
character | String | Optional A character to add |
Move the focus to a typeahead element.
focus: function(i, character) {
if (!this.isActive || !this.inFocus) {
return;
}
var index = this.typeaheadEls.indexOf(this.inFocus.typeahead.el);
var sib = this.typeaheadEls[index + i];
var typeahead;
// If we were passed a character to prepend, find the typeahead for this element
if (character) {
typeahead = this._getTypeaheadByElement(sib);
if (typeahead) {
typeahead.typeahead.addCharacterAtIndex(character, 0);
}
}
if (!sib) {
return false;
}
var sibInput = sib.querySelector('input');
if (sibInput) {
sibInput.focus();
// If we have a typeahead (because we needed to prepend a character), move the caret.
if (typeahead) {
typeahead.typeahead.moveCaret(1);
}
}
return true;
},
Option name | Type | Description |
---|---|---|
character | String | Optional A character to add |
Move the focus to the next element.
focusNext: function(character) {
if (this.focus(1, character)) {
if (this.inFocus && !character)
this.inFocus.typeahead.moveCaretToStart();
}
},
Option name | Type | Description |
---|---|---|
character | String | Optional A character to add |
Move the focus to the next element.
focusPrevious: function(character) {
if (this.focus(-1, character)) {
if (this.inFocus)
this.inFocus.typeahead.moveCaretToEnd();
}
},
Do we have any values?
hasPartialValue: function() {
var i;
for (i in this.typeaheads) {
if (this.typeaheads[i].getValue()) {
return true;
}
}
for (i in this.selects) {
if (this.selects[i].getValue()) {
return true;
}
}
return false;
},
Option name | Type | Description |
---|---|---|
el | Element |
Store a reference to the needed elements.
_cacheElements: function(el) {
this.el = el;
this.inputEl = this.el.querySelector('[type="date"]');
if (!this.inputEl) {
throw new Error('No <input type="date"> element present in date input container!', this.el);
}
this.toggleEl = this.el.querySelector('.spark-date__toggle');
this.messageEl = this.el.querySelector('.spark-input__message') || document.createElement('span');
},
Parse parameters from the elements.
_parseParams: function() {
this.isActive = this.isActive === null ? (this.inputEl.value ? true : false) : this.isActive;
this.isSelect = this.isSelect === null ? (this._hasClass(this.el, 'spark-date--select') ? true : false) : this.isSelect;
this.isTypeahead = this.isTypeahead === null ? (!this.isSelect ? true : false) : this.isTypeahead;
this.format = this.format === null ? (this.inputEl.getAttribute('data-format') ? this.inputEl.getAttribute('data-format') : 'MM-DD-YYYY') : this.format;
this.textFormat = this.textFormat === null ? (this.inputEl.getAttribute('data-text-format') ? this.inputEl.getAttribute('data-text-format') : 'MM DD YYYY') : this.textFormat;
this.showDateAsText = this.showDateAsText === null ? this.inputEl.getAttribute('data-show-date-as-text') !== null : this.showDateAsText;
this.parsedFormat = parseDateFormat(this.format);
this.parsedTextFormat = parseDateFormat(this.textFormat);
this.min = this.inputEl.getAttribute('min') && parsedDomFormat.getValues(this.inputEl.getAttribute('min'));
this.max = this.inputEl.getAttribute('max') && parsedDomFormat.getValues(this.inputEl.getAttribute('max'));
},
Setup the proper inputs. This could mean creating a typeahead, or creating selects.
_initializeInputs: function() {
// @todo: remove this when Android fixes its keyup/keypress/keydown bug
// http://stackoverflow.com/questions/17139039/keycode-is-always-zero-in-chrome-for-android
if (this.isTypeahead) {
this._initializeInputPieces();
this._runTypeaheads();
}
else if (this.isSelect) {
this._removeClass(this.el, 'spark-input');
this._initializeInputPieces();
}
},
Replace the date input with a group of typeaheads or select inputs.
Keep the date input around and store the typeahead data in there in an ISO date format.
_initializeInputPieces: function() {
// Hide the original element. This will be updated as the typeahead values change
this.inputEl.style.display = 'none';
var els = [];
var label;
// Create a new typeahead for each part of the parsed format. Also add placeholder elements.
this.parsedFormat.parts.forEach(function(part) {
// Something weird with Node that makes us have to specify what `this` is here.
(this.isTypeahead ? this._initializeTypeaheadPiece : this._initializeSelectPiece).call(this, els, part);
}.bind(this));
// Create a holder for all the pieces
var piecesEl = document.createElement('span');
piecesEl.className = this.isTypeahead ? 'spark-input__fields' : 'spark-select-group';
// Add all the necessary elements
this._appendChildren(piecesEl, els);
// If this is a select group, move the label element.
if (this.isSelect && (label = this.el.querySelector('.spark-label'))) {
piecesEl.appendChild(label);
}
// Add the pieces holder
this.el.insertBefore(piecesEl, this.inputEl);
// Set the value
if (this.inputEl.value) {
this.setValue(parsedDomFormat.getValues(this.inputEl.value));
this.isActive = true;
}
},
Option name | Type | Description |
---|---|---|
els | Array | |
part | Object |
Create a typeahead or placeholder piece.
_initializeTypeaheadPiece: function(els, part) {
this.typeaheads = this.typeaheads || {};
this.typeaheadEls = this.typeaheadEls || [];
var el;
switch (part.name) {
case 'day':
case 'month':
case 'year':
this.typeaheads[part.name] = new DateTypeahead({
type: part.name,
length: part.length,
placeholder: part.value,
onFocus: this._onTypeaheadFocusBound,
onBlur: this._onTypeaheadBlurBound,
onChange: this._onPieceChangeBound,
onBackspace: this._onTypeaheadBackspaceBound,
onEnd: this._onTypeaheadEndBound
});
el = this.typeaheads[part.name].typeahead.el;
this.typeaheadEls.push(el);
break;
default:
el = document.createElement('span');
el.innerHTML = part.value;
el.className = 'spark-input__divider';
break;
}
els.push(el);
},
Replace the date input with three date dropdowns. Keep the date input around and store the
select data in there.
_initializeSelectPiece: function(els, part) {
this.selects = this.selects || {};
this.selectEls = this.selectEls || [];
if (['day', 'month', 'year'].indexOf(part.name) === -1) {
return;
}
var el;
this.selects[part.name] = new DateSelect({
type: part.name,
onChange: this._onPieceChangeBound
});
el = this.selects[part.name].select.el;
els.push(el);
this.selectEls.push(el);
},
If our element is a label, convert it to a div so that
we are semantically correct. Can't have more than one
input inside of a label!
_convertLabel: function() {
if (this.isTypeahead || this.el.nodeName.toLowerCase() !== 'label') {
return;
}
var newEl = document.createElement('fieldset');
this._copyAttributes(this.el, newEl);
this._appendChildren(newEl, this.el.children);
if (this.el.parentNode) {
this.el.parentNode.replaceChild(newEl, this.el);
}
this.el = newEl;
},
Validate the typeahead values.
_validateTypeaheads: function() {
if (!this.typeaheads) {
return;
}
var month = this.typeaheads.month ? this.typeaheads.month.getValue(true) : null;
var year = this.typeaheads.year ? this.typeaheads.year.getValue(true) : null;
var maxDay = (month && new Date(year !== null ? year : new Date().getFullYear(), month, 0).getDate()) || this._getMaxDaysInMonth(month);
var day = this.typeaheads.day ? this.typeaheads.day.getValue(true) : null;
if (maxDay < day) {
this.typeaheads.day.setValue(maxDay);
this.updateInput();
}
},
Validate the boundaries of the typeahead values relative to the min and max values.
_validateTypeaheadBounds: function() {
var year = this.typeaheads.year ? this.typeaheads.year.getValue(true) : null;
var month = this.typeaheads.month ? this.typeaheads.month.getValue(true) : null;
var day = this.typeaheads.day ? this.typeaheads.day.getValue(true) : null;
if (!year || !month || !day) {
return;
}
var date = new Date(year, month - 1, day);
var set = '';
if (this.min && date < new Date(this.min.year, this.min.month - 1, this.min.day)) {
set = 'min';
}
else if (this.max && date > new Date(this.max.year, this.max.month - 1, this.max.day)) {
set = 'max';
}
if (set) {
this.typeaheads.year.setValue(padNumber(this[set].year, this.typeaheads.year.typeahead.format.length));
this.typeaheads.month.setValue(padNumber(this[set].month, this.typeaheads.month.typeahead.format.length));
this.typeaheads.day.setValue(padNumber(this[set].day, this.typeaheads.day.typeahead.format.length));
this.updateInput();
}
},
Pad the typeahead input values.
_padTypeaheads: function() {
if (this._pauseInputChange) return;
this._pauseInputChange = true;
for (var i in this.typeaheads) {
this._padTypeahead(this.typeaheads[i]);
}
this._pauseInputChange = false;
},
Option name | Type | Description |
---|---|---|
typeahead | Typeahead |
Pad the typeahead input values.
_padTypeahead: function(typeahead) {
var value = typeahead.getValue();
if (value) {
var padded = padNumber(value, typeahead.typeahead.format.length);
if (value !== padded) typeahead.setValue(padNumber(value, typeahead.typeahead.format.length));
}
},
Do any of the typeaheads have a value?
_hasTypeaheadValue: function() {
for (var i in this.typeaheads) {
if (this.typeaheads[i].getValue(true)) {
return true;
}
}
return false;
},
Validate select input values.
_validateSelects: function() {
if (!this.selects) {
return;
}
var month = this.selects.month ? this.selects.month.getValue(true) : null;
var year = this.selects.year ? this.selects.year.getValue(true) : null;
var maxDay = (month && new Date(year !== null ? year : new Date().getFullYear(), month, 0).getDate()) || this._getMaxDaysInMonth(month);
var day = this.selects.day ? this.selects.day.getValue(true) : null;
this.selects.day.setOptions({
max: maxDay
});
if (maxDay < day) {
this.selects.day.setValue(maxDay);
}
this.updateInput();
},
Option name | Type | Description |
---|---|---|
month | Number | The month's number. 1-12. |
return | Number | The maximum number of days. 28-31. |
Get the maximum number of days for a given month.
_getMaxDaysInMonth: function(month) {
if (month === 2) return 29;
else if ([4, 6, 9, 11].indexOf(month) !== -1) return 30;
return 31;
},
Create bound versions of event listener callbacks and store them.
Otherwise we can't unbind from these events later because the
function signatures won't match.
_bindEventListenerCallbacks: function() {
this._onClickBound = this._onClick.bind(this);
this._onTypeaheadFocusBound = this._onTypeaheadFocus.bind(this);
this._onTypeaheadBlurBound = this._onTypeaheadBlur.bind(this);
this._onPieceChangeBound = this._onPieceChange.bind(this);
this._onTypeaheadBackspaceBound = this._onTypeaheadBackspace.bind(this);
this._onTypeaheadEndBound = this._onTypeaheadEnd.bind(this);
this._onInputChangeBound = this._onInputChange.bind(this);
this._onVisibleChildrenBound = this._onVisibleChildren.bind(this);
},
Add event listeners.
_addEventListeners: function() {
this.el.addEventListener('click', this._onClickBound);
this.inputEl.addEventListener('change', this._onInputChangeBound);
document.addEventListener('spark.visible-children', this._onVisibleChildrenBound, true);
},
Remove event listeners.
_removeEventListeners: function() {
this.el.removeEventListener('click', this._onClickBound);
this.inputEl.removeEventListener('change', this._onInputChangeBound);
document.removeEventListener('spark.visible-children', this._onVisibleChildrenBound, true);
},
Option name | Type | Description |
---|---|---|
e | Object |
Handle the spark.visible-children event
_onVisibleChildren: function(e) {
if (e.target.contains(this.el)) {
window.setTimeout(function() {
this.update();
}.bind(this), 0);
}
},
Resize the elements, to account for any changed display property
update: function() {
this._runTypeaheads();
},
Run all typeaheads so they have placeholder values.
_runTypeaheads: function() {
// Make sure we don't get into an infinite loop. Even though the logic
// in the typeaheads should be stopping this from happening, there is
// something in Safari where the focus and blur events fire in different
// order than other browsers so those failsafes do not work.
if (this.runningTypeaheads) {
return;
}
this.runningTypeaheads = true;
if (this.inFocus) {
this.inFocus.pause();
}
for (var i in this.typeaheads) {
if (this.typeaheads[i] !== this.inFocus) {
this.typeaheads[i].run();
}
}
if (this.inFocus) {
this.inFocus.resume();
this.inFocus.run();
}
this.runningTypeaheads = false;
},
Update the active and focus classes.
_updateClass: function() {
this._toggleClass(this.el, 'active', this.isActive);
this._toggleClass(this.el, 'has-partial-value', this.hasPartialValue());
this._toggleClass(this.el, 'focus', this.inFocus ? true : false);
},
Option name | Type | Description |
---|---|---|
el | Element | |
return | Object |
Get the typeahead that corresponds to the given element.
_getTypeaheadByElement: function(el) {
for (var i in this.typeaheads) {
if (this.typeaheads[i].typeahead.el === el) {
return this.typeaheads[i];
}
}
},
Show the date as text.
_showDateText: function() {
var text = this._getDateText();
if (!text || !this.showDateAsText) {
return;
}
if (!this.dateTextEl) {
this._createDateTextEl();
}
this.dateTextEl.innerHTML = text;
this.dateTextEl.style.display = '';
},
Hide the date as text.
_hideDateText: function() {
if (!this.showDateAsText || !this.dateTextEl) {
return;
}
this.dateTextEl.style.display = 'none';
},
Create the date text element.
_createDateTextEl: function() {
var el = document.createElement('div');
el.className = 'spark-input__overlay';
el.style.display = 'none';
this.el.appendChild(el);
this.dateTextEl = el;
},
Get the date as text.
_getDateText: function() {
var parts = this.parsedTextFormat.parts;
var i = 0;
var len = parts.length;
var str = '';
var isValid = true;
var val;
for(; i < len; i++) {
val = this.typeaheads[parts[i].name] && this.typeaheads[parts[i].name].getValue();
switch(parts[i].name) {
case 'month':
str += date.getMonthNameShort(val);
if (!val) {
isValid = false;
break;
}
break;
case 'day':
case 'year':
str += val;
if (!val) {
isValid = false;
break;
}
break;
default:
str += parts[i].value;
break;
}
}
return (isValid ? str : false);
},
Option name | Type | Description |
---|---|---|
val | Number | |
typeahead | Object |
When the value of a typeahead or select changes, validate.
_onPieceChange: function() {
this.validate();
if (this.isTypeahead && this.showDateAsText && !this._hasFocus) {
if (this._showTextTimer) {
clearTimeout(this._showTextTimer);
}
this._showTextTimer = setTimeout(function() {
this._showDateText();
}.bind(this), 0);
}
},
Option name | Type | Description |
---|---|---|
val | Number | |
typeahead | Object |
When the typeahead gains focus.
_onTypeaheadFocus: function(val, typeahead) {
if (this.runningTypeaheads) return;
this._hideDateText();
if (!this._hasFocus) {
this._hasFocus = true;
(this.onFocus || noop)(this, this.inputEl.value);
}
this._triggerEvent(this.inputEl, 'focus');
this.inFocus = typeahead;
this.show();
this._updateClass();
if (this._blurTimer) {
clearTimeout(this._blurTimer);
this._blurTimer = null;
}
},
Option name | Type | Description |
---|---|---|
val | Number | |
typeahead | Object |
When the typeahead loses focus, make sure numbers are padded properly.
_onTypeaheadBlur: function(val, typeahead) {
if (this.runningTypeaheads) return;
this.inFocus = null;
this._padTypeahead(typeahead);
this.updateInput();
this._updateClass();
if (!this.inputEl.value && !this._hasTypeaheadValue()) {
this.hide();
}
else {
this._validateTypeaheadBounds();
}
this._blurTimer = setTimeout(function() {
this._hasFocus = false;
(this.onBlur || noop)(this, this.inputEl.value);
this._showDateText();
}.bind(this), 1);
},
Option name | Type | Description |
---|---|---|
val | Number | |
typeahead | Object |
When the typeahead fires a backspace event, move back to the previous input.
_onTypeaheadBackspace: function() {
this.focusPrevious();
},
Option name | Type | Description |
---|---|---|
typeahead | Object | |
character | String | Optional |
When the typeahead is at its maximum length and the caret is at the end,
focus on the next input field.
_onTypeaheadEnd: function(typeahead, character) {
this.focusNext(character);
},
Option name | Type | Description |
---|---|---|
e | Object |
When the input that corresponds to this instance changes. Allows us to listen
and respond to changes made by other components (Calendar Popover, for example).
_onInputChange: function(e) {
if (this.isTypeahead) {
this.isActive = e.target.value ? true : false;
this._updateClass();
}
if (this._pauseInputChange) return;
this.setValue(parsedDomFormat.getValues(e.target.value));
(this.onChange || noop)(this.inputEl.value, this.inputEl);
},
Option name | Type | Description |
---|---|---|
e | Object |
When the input group is clicked, focus on the first typeahead
if we don't already have focus.
_onClick: function(