rc-adminlte
Version:
AdminLTE template ported to React
366 lines (335 loc) • 11 kB
JSX
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import $ from 'jquery';
import 'select2/dist/js/select2';
import 'select2/dist/css/select2.css';
import './Select2.css';
import InputWrapper from './InputWrapper';
import { arrayEquals } from '../../Utilities';
import { OptionShape, ValueShape, ListOfValueShape, ArrayOfValueShape } from './InputShapes';
class Select2 extends Component {
state = {}
boundHandlers = {}
internalOptions = {}
actualOptions = {}
constructor(props) {
super(props);
const { options } = props;
this.mapOptions(options);
this.handleSelect = this.handleSelect.bind(this);
}
componentDidMount() {
const {
placeholder, multiple, options, defaultWidgetOptions, allowClear, value,
onFetchData, select2Options, fetchDataDelay,
...props
} = this.props;
const actualWidgetOptions = {
...select2Options,
...defaultWidgetOptions,
};
const data = this.internalOptions;
const $temp = $(this.domRef);
let fakeAjax;
if (actualWidgetOptions.language) {
const newLanguage = {};
Object.entries(actualWidgetOptions.language).forEach(([key, value]) => {
if (typeof value === 'function') {
newLanguage[key] = value;
} else {
newLanguage[key] = () => value;
}
});
actualWidgetOptions.language = newLanguage;
}
if (onFetchData) {
const newTransport = (params, success, failure) => {
const { data: { term, page } } = params;
const highjackedSuccess = (data2, hasMore) => {
const newOptions = this.actualOptions.concat(data2);
this.mapOptions(newOptions);
success({ results: this.optionsToSelect2(data2), pagination: { more: hasMore } });
};
const highjackedFailure = () => {
failure();
};
onFetchData({ page: page || 0, searchValue: term || '' }, highjackedSuccess, highjackedFailure);
return { status: 404 };
};
fakeAjax = { transport: newTransport, delay: fetchDataDelay };
}
const $ref = $temp.select2({
placeholder,
data,
allowClear,
ajax: fakeAjax,
...actualWidgetOptions,
});
const handleEvent = (event, callback) => {
const value2 = this.getValue($ref.select2('data'), multiple);
// eslint-disable-next-line no-param-reassign
event.params = {};
// eslint-disable-next-line no-param-reassign
event.params.data = value2;
callback(event);
};
const boundHandlers = {};
const bindEvent = ([eventName, callback]) => {
const jQueryEvent = this.toJQueryEvent(eventName);
const actualCallback = (e) => { handleEvent(e, callback); };
boundHandlers[eventName] = { callback, actualCallback, jQueryEvent };
$ref.on(jQueryEvent, actualCallback);
};
// Find all Eventhandler Props and bind them
Object.entries(props)
.filter(([key, value2]) => key.startsWith('on') && value2)
.forEach(bindEvent);
this.boundHandlers = boundHandlers;
$ref.on('select2:select', () => this.handleSelect);
$ref.val(value).trigger('change.select2');
this.$ref = $ref;
}
shouldComponentUpdate({
options, value, disabled, ...props
}) {
const {
options: propOptions, multiple, value: propValue, disabled: propDisabled,
} = this.props;
if (disabled !== propDisabled) {
return true;
}
// Options changed
if (!arrayEquals(options, propOptions)) {
// console.log(`Select2 with label ${this.props.label} will update because of options`);
return true;
}
// Value changed
if ((value || propValue) && multiple ? !arrayEquals(value, propValue) : value !== propValue) {
// console.log(`Select2 with label ${this.props.label} will update because of value`);
return true;
}
const BreakException = {};
const { boundHandlers } = this;
// Eventhandler changed
try {
Object.entries(props)
.filter(([key]) => key.startsWith('on'))
.forEach(([key, callback]) => {
if (callback !== boundHandlers[key].callback) {
throw BreakException;
}
});
} catch (e) {
// console.log(`Select2 with label ${this.props.label} will update because of callback`);
return true;
}
return false;
}
componentDidUpdate({ value: oldValue, disabled: oldDisabled, options: oldOptions }) {
// console.log(`Select2 with label ${this.props.label} did update`);
const {
value, multiple, disabled, options,
} = this.props;
const $ref = $(this.domRef);
if (disabled !== oldDisabled) {
$ref.prop('disabled', disabled);
}
// If value changed update select2 value
if (value !== oldValue) {
// console.log(`Updating value from Select2 with label ${this.props.label}`);
$ref.val(value).trigger('change.select2');
}
// Loop Eventhandler Props and unbinded removed events and bind new events
Object.entries(this.props)
.filter(([key]) => key.startsWith('on'))
.forEach(([eventName, callback]) => {
const boundHandler = this.boundHandlers[eventName] || {};
{
const { callback: storedCallback, actualCallback, jQueryEvent } = boundHandler;
// Eventhandler exists and new handler is different from old
if (storedCallback && callback !== storedCallback) {
$ref.unbind(jQueryEvent, actualCallback);
}
}
const { callback: storedCallback } = boundHandler;
if (callback && callback !== storedCallback) {
const jQueryEvent = this.toJQueryEvent(eventName);
const handleEvent = (event, callback2) => {
const value2 = this.getValue($ref.select2('data'), multiple);
// eslint-disable-next-line no-param-reassign
event.params = {};
// eslint-disable-next-line no-param-reassign
event.params.data = value2;
callback2(event);
};
const actualCallback = (e) => { handleEvent(e, callback); };
// Store bound event handler to be able to later remove it
this.boundHandlers[eventName] = { callback, actualCallback, jQueryEvent };
$ref.on(jQueryEvent, actualCallback);
}
});
if (!arrayEquals(options, oldOptions)) {
const currVal = $ref.val();
$ref.find('option').remove();
this.mapOptions(options);
this.internalOptions.forEach(({ id, text }) => {
// eslint-disable-next-line no-undef
const newOption = new Option(text, id, false, false);
$ref.append(newOption);
});
$ref.val(currVal);
$ref.trigger('select2:change');
}
}
componentWillUnmount() {
$(this.domRef)
.select2('destroy');
}
singleOptionToSelect2 = (p) => {
if (typeof p === 'object') {
const { value: id, label: text, ...props } = p;
return { id, text: text || id, ...props };
}
return { id: p, text: p };
}
optionsToSelect2 =
scopedOptions => scopedOptions && scopedOptions.map(this.singleOptionToSelect2);
optionsFromSelect2 = select2Options => select2Options.map(({ id }) => this.mapped[id].actual);
getValue = (select2Options, multiple) => {
let value = this.optionsFromSelect2(select2Options);
if (!multiple && value.length > 0) {
[value] = value;
}
return value;
};
handleSelect() {
const { value: propValue, multiple } = this.props;
const { $ref } = this;
const newValue = this.getValue($ref.select2('data'), multiple);
if (propValue && newValue !== propValue) {
$ref.val(propValue).trigger('change.select2');
}
}
mapOptions(options) {
this.actualOptions = options;
this.internalOptions = this.optionsToSelect2(options);
const mapped = {};
options.forEach((p, i) => {
if (typeof p === 'object') {
const { value } = p;
mapped[value] = {};
mapped[value].actual = value;
mapped[value].internal = this.internalOptions[i];
} else {
mapped[p] = {};
mapped[p].actual = p;
mapped[p].internal = this.internalOptions[i];
}
});
this.mapped = mapped;
}
// eslint-disable-next-line class-methods-use-this
toJQueryEvent(eventName) {
let jQueryEvent;
switch (eventName) {
case 'onChange':
jQueryEvent = 'change';
break;
case 'onBeforeClose':
jQueryEvent = 'select2:closing';
break;
case 'onClose':
jQueryEvent = 'select2:close';
break;
case 'onBeforeOpening':
jQueryEvent = 'select2:opening';
break;
case 'onOpen':
jQueryEvent = 'select2:open';
break;
case 'onBeforeSelect':
jQueryEvent = 'select2:selecting';
break;
case 'onSelect':
jQueryEvent = 'select2:select';
break;
case 'onBeforeUnselect':
jQueryEvent = 'select2:unselecting';
break;
case 'onUnselect':
jQueryEvent = 'select2:unselect';
break;
default:
break;
}
return jQueryEvent;
}
render() {
const {
name, disabled, multiple, value, defaultValue, id, ...props
} = this.props;
return (
<InputWrapper {...props}>
<select
id={id}
ref={(c) => { this.domRef = c; }}
name={name}
className="form-control"
disabled={disabled}
multiple={multiple}
defaultValue={defaultValue}
style={{ width: '100%' }}
/>
</InputWrapper>
);
}
}
Select2.propTypes = {
id: PropTypes.string,
placeholder: PropTypes.string,
multiple: PropTypes.bool,
options: ListOfValueShape,
value: PropTypes.oneOfType([ValueShape, ArrayOfValueShape]),
defaultValue: PropTypes.oneOfType([ValueShape, ArrayOfValueShape]),
disabled: PropTypes.bool,
defaultWidgetOptions: PropTypes.shape({}),
select2Options: PropTypes.shape({}),
name: PropTypes.string,
allowClear: PropTypes.bool,
onChange: PropTypes.func,
onBeforeClose: PropTypes.func,
onClose: PropTypes.func,
onBeforeOpening: PropTypes.func,
onOpen: PropTypes.func,
onBeforeSelect: PropTypes.func,
onSelect: PropTypes.func,
onBeforeUnselect: PropTypes.func,
onUnselect: PropTypes.func,
onFetchData: PropTypes.func,
fetchDataDelay: PropTypes.number,
};
Select2.defaultProps = {
id: null,
placeholder: '',
multiple: false,
options: [],
value: null,
defaultValue: null,
disabled: false,
defaultWidgetOptions: {},
select2Options: {},
name: null,
allowClear: false,
onChange: null,
onBeforeClose: null,
onClose: null,
onBeforeOpening: null,
onOpen: null,
onBeforeSelect: null,
onSelect: null,
onBeforeUnselect: null,
onUnselect: null,
onFetchData: undefined,
fetchDataDelay: 100,
};
export default Select2;