UNPKG

rc-adminlte

Version:

AdminLTE template ported to React

366 lines (335 loc) 11 kB
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;