ldx-widgets
Version:
widgets
493 lines (460 loc) • 17.1 kB
JavaScript
(function() {
var ESCAPE, MultiSelect, MultiSelectOption, PropTypes, React, ReactDOM, SearchInput, button, createClass, div, filter, find, isEqual, li, makeGuid, map, ref1, ul;
React = require('react');
PropTypes = require('prop-types');
createClass = require('create-react-class');
ReactDOM = require('react-dom');
find = require('lodash/find');
filter = require('lodash/filter');
isEqual = require('lodash/isEqual');
map = require('lodash/map');
MultiSelectOption = React.createFactory(require('./multi_select_option'));
SearchInput = React.createFactory(require('./search_input'));
makeGuid = require('../utils').makeGuid;
ESCAPE = require('../constants/keyboard').ESCAPE;
ref1 = require('react-dom-factories'), div = ref1.div, button = ref1.button, ul = ref1.ul, li = ref1.li;
/*
Multi Select Props
@props.options - REQUIRED - Array
An array of options for the user to click - can be a flat array (where each entry is both the value and label)
or an array of objects, where the value is the `valueField` and the label is the `labelField` (see below)
@props.values - OPTIONAL - Array
A flat array of values that the control initializes to. If the options array is objects, each entry should be the
`valueField` value. Will default to an empty array
@props.labelField - OPTIONAL - String
The attribute name from each option object that is the 'value`
@props.valueField - OPTIONAL - String
The attribute name from each option object that is the user facing label
@props.onChange - OPTIONAL - function
Function/method to fire when the data changes
@props.filter - OPTIONAL - Boolean - default: 'auto'
Show text filter? auto will show it whenever the options list is longer than 4
@props.allowDefault - OPTIONAL - Boolean - default: false
Allow one entry to be set as the default
@props.valueOfDefault - OPTIONAL - used in conjunction w/ allowDefault
The value of the option that is currently set as the default
@props.searchPlaceholder - OPTIONAL
placeholder text for the add button
@props.editPlaceholder - OPTIONAL
placeholder text for the filter field
@props.tabIndex - OPTIONAL
tab order of the edi button
@props.returnFullObjects - OPTIONAL - Boolean - default: false
whether or not the getFormData method should return a collection of selected objects or a flat array
@props.onRemove - OPTIONAL - function
function call when an item is removed, will pass the removed item
*/
MultiSelect = createClass({
displayName: 'MultiSelect',
contextTypes: {
clearValidationError: PropTypes.func,
addValidationError: PropTypes.func,
getValidationStatus: PropTypes.func,
toggleValidationError: PropTypes.func
},
getDefaultProps: function() {
return {
filter: 'auto',
allowDefault: false,
editPlaceholder: 'Edit Items',
searchPlaceholder: 'Filter Items',
tabIndex: -1,
returnFullObjects: false,
isInPopover: false,
onRemove: function() {}
};
},
render: function() {
var allowDefault, buttonText, disabled, editPlaceholder, error, filterTerm, forceShowAllErrors, getValidationStatus, i, isActive, isValid, j, k, len, len1, notSelected, option, otherOptions, outerClass, ref2, ref3, ref4, searchPlaceholder, selected, selectedOptions, tabIndex, theDefault, valueHasChanged, visible;
ref2 = this.state, selected = ref2.selected, notSelected = ref2.notSelected, isActive = ref2.isActive, theDefault = ref2.theDefault, filterTerm = ref2.filterTerm, valueHasChanged = ref2.valueHasChanged;
ref3 = this.props, editPlaceholder = ref3.editPlaceholder, searchPlaceholder = ref3.searchPlaceholder, allowDefault = ref3.allowDefault, tabIndex = ref3.tabIndex, disabled = ref3.disabled;
getValidationStatus = this.context.getValidationStatus;
ref4 = getValidationStatus(this.inputId), error = ref4.error, forceShowAllErrors = ref4.forceShowAllErrors;
isValid = error == null;
outerClass = 'multiselect field-wrap';
if (!isValid && (valueHasChanged || forceShowAllErrors)) {
outerClass += ' invalid';
}
selectedOptions = [];
otherOptions = [];
for (i = j = 0, len = selected.length; j < len; i = ++j) {
option = selected[i];
selectedOptions.push(MultiSelectOption({
key: option.value,
ref: option.value,
option: option,
allowDefault: allowDefault,
isActive: isActive,
setValues: this.setValues,
disabled: disabled,
setDefault: this.setDefault,
tabIndex: tabIndex,
isTheDefault: option === theDefault,
onRemove: this.props.onRemove,
onBlur: i === 0 ? this.btnBlur : null
}));
}
if (selectedOptions.length === 0) {
selectedOptions.push(li({
key: 'none',
className: 'multiselect-none'
}, ['None']));
}
visible = filter(notSelected, {
isVisible: true
});
if (isActive) {
for (i = k = 0, len1 = visible.length; k < len1; i = ++k) {
option = visible[i];
otherOptions.push(MultiSelectOption({
key: option.value,
ref: option.value,
option: option,
allowDefault: allowDefault,
isActive: isActive,
tabIndex: tabIndex,
setValues: this.setValues,
onBlur: i === visible.length - 1 ? this.btnBlur : null
}));
}
}
buttonText = "+ " + editPlaceholder;
return div({
className: outerClass,
onClick: this.toggleOn
}, [
ul({
key: 'selectedList',
ref: 'selectedList',
className: 'multiselect-list-in'
}, selectedOptions), this.filterShouldBeShown() ? SearchInput({
ref: 'filterField',
key: 'filter-input',
placeholder: searchPlaceholder,
handleChange: this.handlefilter,
wrapClass: 'multi-select-filter',
width: '100%',
focusOnMount: true,
disabled: disabled,
tabIndex: tabIndex,
term: filterTerm
}) : void 0, !(isActive || disabled) ? button({
key: 'addButton',
ref: 'addButton',
className: 'multiselect-edit',
tabIndex: tabIndex
}, buttonText) : void 0, isActive ? ul({
key: 'notSelectedList',
ref: 'notSelectedList',
className: 'multiselect-list-out'
}, otherOptions) : void 0, div({
key: 'errors',
className: 'field-errors-show',
ref: 'errorAnchor',
onMouseOver: this.handleErrorMouseOver,
onMouseOut: this.handleErrorMouseOut
})
]);
},
componentWillMount: function() {
return this.inputId = makeGuid();
},
componentDidMount: function() {
var selected, validation;
document.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('click', this.blur, false);
selected = this.state.selected;
validation = this.props.validation;
return this.validate(validation, selected);
},
componentWillReceiveProps: function(nextProps) {
var ref2, selected, stateOptions, validation, validationChanged;
validation = nextProps.validation;
selected = this.state.selected;
validationChanged = (typeof validation === 'function' ? false : !isEqual(validation != null ? validation.messages : void 0, (ref2 = this.props.validation) != null ? ref2.messages : void 0));
if (validationChanged) {
this.validate(validation, selected);
}
if (nextProps.options !== this.props.options || nextProps.values !== this.props.values) {
stateOptions = this.setOptions(nextProps);
return this.setState({
selected: stateOptions.selected || [],
notSelected: filter(this.allOptions, {
isSelected: false
}) || [],
theDefault: stateOptions.theDefault
});
}
},
componentWillUnmount: function() {
document.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('click', this.blur, false);
return this.context.clearValidationError({
groupId: this.inputId,
isInPopover: this.props.isInPopover
});
},
getInitialState: function() {
var stateOptions;
stateOptions = this.setOptions(this.props);
return {
selected: stateOptions.selected || [],
notSelected: filter(this.allOptions, {
isSelected: false
}) || [],
isActive: false,
theDefault: stateOptions.theDefault,
filterTerm: '',
valueHasChanged: false
};
},
setOptions: function(params) {
var allowDefault, data, j, labelField, len, newOption, obj, option, options, selected, theDefault, valueField, valueOfDefault, values;
options = params.options, values = params.values, labelField = params.labelField, valueField = params.valueField, valueOfDefault = params.valueOfDefault, allowDefault = params.allowDefault;
this.allOptions = [];
for (j = 0, len = options.length; j < len; j++) {
option = options[j];
newOption = {
isSelected: false,
isVisible: true
};
if (typeof option === 'object') {
if (!(((valueField != null) && (labelField != null)) || ((option.value != null) && (options.label != null)))) {
return typeof console !== "undefined" && console !== null ? console.error('MultiSelect requires labelField and valueField props when the options array is made up of objects') : void 0;
}
newOption.value = option[valueField];
newOption.label = option[labelField];
} else {
newOption.label = option.toString();
newOption.value = option;
}
if ((values != null) && (values.indexOf(newOption.value) !== -1 || find(values, (
obj = {},
obj["" + valueField] = newOption.value,
obj
)))) {
newOption.isSelected = true;
}
this.allOptions.push(newOption);
theDefault = find(this.allOptions, {
value: valueOfDefault
});
selected = filter(this.allOptions, {
isSelected: true
});
if (theDefault == null) {
if (selected.length != null) {
theDefault = selected[0];
}
}
}
data = {
selected: selected,
theDefault: theDefault
};
return data;
},
getValue: function() {
return this.getFormData();
},
getFormData: function() {
var j, len, option, ref2, results, selectedValues;
selectedValues = map(filter(this.allOptions, {
isSelected: true
}), 'value');
if (!this.props.returnFullObjects) {
return selectedValues;
}
ref2 = this.props.options;
results = [];
for (j = 0, len = ref2.length; j < len; j++) {
option = ref2[j];
if (selectedValues.indexOf(option[this.props.valueField]) !== -1) {
results.push(option);
}
}
return results;
},
toggleOn: function(e) {
var base;
if (this.props.disabled) {
return;
}
if (typeof (base = e.nativeEvent).stopImmediatePropagation === "function") {
base.stopImmediatePropagation();
}
if (!this.state.isActive) {
return this.setState({
isActive: true
}, (function(_this) {
return function() {
if (!_this.filterShouldBeShown()) {
return _this.focusFirstOption();
}
};
})(this));
}
},
focusFirstOption: function() {
var firstSelected, firstUnselected;
firstUnselected = this.refs.notSelectedList.getElementsByTagName('button')[0];
firstSelected = this.refs.selectedList.getElementsByTagName('button')[0];
if (firstUnselected != null) {
return firstUnselected.focus();
} else {
return firstSelected.focus();
}
},
btnBlur: function() {
var ref, ref2, ref3, refname;
ref2 = this.refs;
for (refname in ref2) {
ref = ref2[refname];
if (refname === 'addButton') {
continue;
}
if (((ref3 = ref.refs) != null ? ref3.toggleBtn : void 0) === document.activeElement) {
return;
}
if (ref === document.activeElement) {
return;
}
}
return this.blur();
},
blur: function(cb) {
return this.setState({
isActive: false
}, function() {
return typeof cb === "function" ? cb() : void 0;
});
},
setDefault: function(newDefault) {
return this.setState({
theDefault: newDefault
});
},
setValues: function(option) {
var jsonPath, onChange, ref2, selected, theDefault, validation;
theDefault = this.state.theDefault;
ref2 = this.props, validation = ref2.validation, onChange = ref2.onChange, jsonPath = ref2.jsonPath;
option.isSelected = !option.isSelected;
selected = filter(this.allOptions, {
isSelected: true
});
if (selected.indexOf(theDefault) === -1) {
if (selected.length) {
theDefault = selected[0];
}
}
return this.setState({
selected: filter(this.allOptions, {
isSelected: true
}),
notSelected: filter(this.allOptions, {
isSelected: false
}),
theDefault: theDefault,
valueHasChanged: true
}, function() {
if (this.filterShouldBeShown()) {
this.refs.filterField.focus();
} else {
this.focusFirstOption();
}
if (typeof onChange === "function") {
onChange(this.getValue(), jsonPath);
}
return this.validate(validation, this.state.selected);
});
},
handlefilter: function(term) {
var item, j, k, len, len1, notSelected, ref2;
notSelected = filter(this.allOptions, {
isSelected: false
});
if (term === '') {
ref2 = this.allOptions;
for (j = 0, len = ref2.length; j < len; j++) {
item = ref2[j];
item.isVisible = true;
}
} else {
for (k = 0, len1 = notSelected.length; k < len1; k++) {
item = notSelected[k];
if (item.label.toLowerCase().search(term.toLowerCase()) !== -1) {
item.isVisible = true;
} else {
item.isVisible = false;
}
}
}
return this.setState({
filterTerm: term,
notSelected: notSelected
});
},
filterShouldBeShown: function() {
return this.state.isActive && (this.props.filter === true || (this.props.filter === 'auto' && this.allOptions.length > 4));
},
validate: function(validation, value) {
var addValidationError, clearValidationError, isInPopover, ref2, ref3, tabId, validationError;
if (validation === false) {
return;
}
if (typeof validation === 'function') {
validationError = validation(value);
} else {
validationError = validation;
}
ref2 = this.props, isInPopover = ref2.isInPopover, tabId = ref2.tabId;
ref3 = this.context, addValidationError = ref3.addValidationError, clearValidationError = ref3.clearValidationError;
if (validationError != null) {
return addValidationError({
anchor: this.refs.errorAnchor,
error: validationError,
groupId: this.inputId,
isInPopover: isInPopover,
tabId: tabId
});
} else {
return clearValidationError({
groupId: this.inputId,
isInPopover: isInPopover
});
}
},
handleErrorMouseOver: function() {
var isInPopover, toggleValidationError;
isInPopover = this.props.isInPopover;
toggleValidationError = this.context.toggleValidationError;
return toggleValidationError({
groupId: this.inputId,
status: true,
isInPopover: isInPopover
});
},
handleErrorMouseOut: function() {
var isInPopover, toggleValidationError;
isInPopover = this.props.isInPopover;
toggleValidationError = this.context.toggleValidationError;
return toggleValidationError({
groupId: this.inputId,
status: false,
isInPopover: isInPopover
});
},
handleKeyDown: function(e) {
if (e.keyCode === ESCAPE) {
if (this.state.isActive) {
return this.blur((function(_this) {
return function() {
return _this.refs.addButton.focus();
};
})(this));
}
}
}
});
module.exports = MultiSelect;
}).call(this);