UNPKG

carbon-react

Version:

A library of reusable React components and an interface for easily building user interfaces based on Flux.

632 lines (516 loc) • 17.9 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; var _react = require('react'); var _react2 = _interopRequireDefault(_react); var _propTypes = require('prop-types'); var _propTypes2 = _interopRequireDefault(_propTypes); var _dropdown = require('./../dropdown'); var _dropdown2 = _interopRequireDefault(_dropdown); var _i18nJs = require('i18n-js'); var _i18nJs2 = _interopRequireDefault(_i18nJs); var _classnames = require('classnames'); var _classnames2 = _interopRequireDefault(_classnames); var _escapeStringRegexp = require('escape-string-regexp'); var _escapeStringRegexp2 = _interopRequireDefault(_escapeStringRegexp); var _lodash = require('lodash'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /** * A dropdown filter widget. * * == How to use a dropdown in a component: * * In your file * * import DropdownFilter from 'carbon/lib/components/dropdown-filter'; * * To render a DropdownFilter: * * <DropdownFilter name="foo" options={ foo } onChange={ myChangeHandler } /> * * The developer should pass data to the store as JSON. e.g. * * foo: [{ id: 1, name: "Foo" }, { id: 2, name: "Bar" }] * * You can also use the component in 'suggest' mode, which only shows the dropdown * once a filter term has been entered. * * You can also use the component in 'freetext' mode, which behaves like 'suggest', * but allows write-in text values in addition to list options. Specify an initial * write-in value with the `visibleValue` property, instead of the `value` property * for an option id. Set the `freetextName` property to add a second hidden input * for the write-in value, as opposed to the `name` property used for the option id. * * You can also define a function using the 'create' prop, this will allow you * to trigger events to create new items. * * @class DropdownFilter * @constructor */ var DropdownFilter = function (_Dropdown) { _inherits(DropdownFilter, _Dropdown); /** * Constructor * * @constructor * @param {Array} args - Arguments */ function DropdownFilter() { var _ref; _classCallCheck(this, DropdownFilter); for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } /** * The user input search text. * * @property filter * @type {String} * @default null */ var _this = _possibleConstructorReturn(this, (_ref = DropdownFilter.__proto__ || Object.getPrototypeOf(DropdownFilter)).call.apply(_ref, [this].concat(args))); _this.handleBlur = function () { if (!_this.blockBlur) { var filter = null; if (_this.props.create || _this.props.freetext) { filter = _this.state.filter; } _this.setState({ open: false, filter: filter }); if (_this.props.freetext) { var opt = void 0; if (_this.state.filter) { opt = _this.props.options.find(function (opt) { if (opt.get('name')) { return opt.get('name').toLowerCase() === _this.state.filter.toLowerCase(); } }); } if (opt) { _this.selectValue(opt.get('id'), opt.get('name')); } else { _this.emitOnChangeCallback('', _this.state.filter); } } if (_this.props.onBlur) { _this.props.onBlur(); } } }; _this.handleFocus = function () { if (!_this.writeable && !_this.blockFocus) { _this.setState({ open: true }); } else { _this.blockFocus = false; } _this._input.setSelectionRange(0, _this._input.value.length); }; _this.handleCreate = function (ev) { _this.setState({ open: false }); _this.props.create(ev, _this); }; _this.prepareList = function (options) { if ((_this.writeable || !_this.openingList) && typeof _this.state.filter === 'string') { var filter = _this.state.filter; var regex = new RegExp((0, _escapeStringRegexp2.default)(filter), 'i'); // if user has entered a search filter options = options.filter(function (option) { if (option.name && option.name.search(regex) > -1) { option.name = _this.highlightMatches(option.name, filter); return option; } }); } return options; }; _this.highlighted = function (options) { var highlighted = null; if (_this.state.highlighted) { highlighted = _this.state.highlighted; } else { if (!_this.state.filter && _this.props.value) { highlighted = _this.props.value; } else if (_this.state.filter && options.length) { highlighted = options[0].id; } } return highlighted; }; _this.highlightMatches = function (optionText, value) { if (!value.length) { return optionText; } var beginning = void 0, end = void 0, middle = void 0, newValue = void 0, parsedOptionText = void 0, valIndex = void 0; parsedOptionText = optionText.toLowerCase(); valIndex = parsedOptionText.indexOf(value); if (valIndex === -1) { return optionText; } beginning = optionText.substr(0, valIndex); middle = optionText.substr(valIndex, value.length); end = optionText.substr(valIndex + value.length, optionText.length); // find end of string recursively if (end.indexOf(value) !== -1) { end = _this.highlightMatches(end, value); } // build JSX object newValue = [_react2.default.createElement( 'span', { key: 'beginning' }, beginning ), _react2.default.createElement( 'strong', { key: 'middle' }, _react2.default.createElement( 'u', null, middle ) ), _react2.default.createElement( 'span', { key: 'end' }, end )]; return newValue; }; _this.state.filter = _this.hasFreetextValue() ? _this.props.visibleValue : null; /** * Determines if list is being opened on current render. * * @property openingList * @type {Boolean} * @default false */ _this.openingList = false; // bind scope to functions - allowing them to be overridden and // recalled with the use of super _this.handleVisibleChange = _this.handleVisibleChange.bind(_this); return _this; } _createClass(DropdownFilter, [{ key: 'componentWillUpdate', /** * Lifecycle hook for when the component will update. * * @method componentWillUpdate * @param {Object} nextProps * @param {Object} nextState */ value: function componentWillUpdate(nextProps, nextState) { // if list is being opened, set boolean if (this.state.open != nextState.open) { this.openingList = true; } } /** * Selects the value for the component * * @method selectValue * @param {String} val */ }, { key: 'selectValue', value: function selectValue(val, visibleVal) { var filter = this.props.freetext ? visibleVal : null; _get(DropdownFilter.prototype.__proto__ || Object.getPrototypeOf(DropdownFilter.prototype), 'selectValue', this).call(this, val, visibleVal); this.setState({ filter: filter }); } /* * Handles changes to the visible input field. Updates filter and displayed value. * * @method handleVisibleChange * @param {Object} ev event */ }, { key: 'handleVisibleChange', value: function handleVisibleChange(ev) { var state = { filter: ev.target.value, highlighted: null }; if (this.writeable && ev.target.value.length <= 0) { state.open = false; } else { state.open = true; } this.setState(state); this.openingList = false; if (this.props.create) { // if create is enabled then empty the selected value so the filter persists this.emitOnChangeCallback("", ev.target.value); } } /* * Handles what happens on blur of the input. * * @method handleBlur */ /** * Handles what happens on focus of the input. * * @method handleFocus */ /** * Handles what happens when create button is clicked. * * @method handleCreate */ /** * Prepares list options by converting to JSON and formatting filtered options. * * @method prepareList * @param {Object} options Immutable map of list options */ }, { key: 'results', /** * Function that returns search results. Builds each list item with relevant handlers and classes. * * @method results */ value: function results(options) { var items = _get(DropdownFilter.prototype.__proto__ || Object.getPrototypeOf(DropdownFilter.prototype), 'results', this).call(this, options); if (!items.length) { items = _react2.default.createElement( 'li', { className: 'carbon-dropdown__list-item carbon-dropdown__list-item--no-results' }, _i18nJs2.default.t("dropdownlist.no_results", { defaultValue: "No results match \"%{term}\"", term: this.state.filter }) ); } return items; } /** * Return the list item which should be highlighted by default. * * @method highlighted */ }, { key: 'showArrow', /** * Overrides Dropdown method to conditionally show arrow * * @method showArrow * @return {Boolean} */ value: function showArrow() { return !this.writeable; } /** * Returns the list options in the correct format * * @method options */ }, { key: 'hasFreetextValue', /** * Returns whether properties indicate a freetext write-in value * * @method hasFreetextValue * @return {Boolean} */ value: function hasFreetextValue() { return this.props.freetext && this.props.visibleValue && !this.props.value; } }, { key: 'componentTags', value: function componentTags(props) { return { 'data-component': 'dropdown-filter', 'data-element': props['data-element'], 'data-role': props['data-role'] }; } }, { key: 'listHTML', /** * Getter to return HTML for list to render method. * * @method listHTML */ get: function get() { var original = _get(DropdownFilter.prototype.__proto__ || Object.getPrototypeOf(DropdownFilter.prototype), 'listHTML', this), html = [original]; if (this.state.open && this.props.create) { var text = "Create "; if (this.state.filter) { text += '"' + this.state.filter + '"'; } else { text += "New"; } html.push(_react2.default.createElement( 'a', { className: 'carbon-dropdown__action', 'data-element': 'create', key: 'dropdown-action', onClick: this.handleCreate }, text )); } return html; } }, { key: 'options', get: function get() { return this.prepareList(this.props.options.toJS()); } /** * Uses the mainClasses method provided by the decorator to add additional classes. * * @method mainClasses */ }, { key: 'mainClasses', get: function get() { return (0, _classnames2.default)(_get(DropdownFilter.prototype.__proto__ || Object.getPrototypeOf(DropdownFilter.prototype), 'mainClasses', this), 'carbon-dropdown-filter', { 'carbon-dropdown-filter--writeable': this.writeable }); } /** * Uses the inputClasses method provided by the decorator to add additional classes. * * @method inputClasses */ }, { key: 'inputClasses', get: function get() { var filtered = !this.props.create && !this.props.freetext && typeof this.state.filter === 'string'; return (0, _classnames2.default)(_get(DropdownFilter.prototype.__proto__ || Object.getPrototypeOf(DropdownFilter.prototype), 'inputClasses', this), { 'carbon-dropdown__input--filtered': filtered }); } /** * Input props for the dropdown, extended from the base dropdown component. * * @method inputProps */ }, { key: 'inputProps', get: function get() { var props = _get(DropdownFilter.prototype.__proto__ || Object.getPrototypeOf(DropdownFilter.prototype), 'inputProps', this); var value = props.value; if (typeof this.state.filter === 'string') { // if filter has a value, use that instead value = this.state.filter; } props.readOnly = this.props.readOnly || false; props.onChange = this.handleVisibleChange; props.value = value; return props; } /** * Input props for freetext hidden input. * * @method alternateHiddenInputProps * @return {Object} */ }, { key: 'alternateHiddenInputProps', get: function get() { var props = { ref: "altHidden", type: "hidden", readOnly: true, name: this.props.freetextName, value: this.props.visibleValue }; return props; } /** * Getter to return HTML for alternate hidden input to render method. * * @method alternateHiddenHTML * @return {Object} JSX */ }, { key: 'alternateHiddenHTML', get: function get() { if (!this.props.freetext || !this.props.freetextName) { return null; } return _react2.default.createElement('input', this.alternateHiddenInputProps); } /** * Find and highlights search terms in text * * @method highlightMatches * @param {String} optionText - the text to search * @param {String} value - the search term */ }, { key: 'writeable', /** * Returns whether input is writeable (for suggest or freetext modes) * * @method writeable * @return {Boolean} */ get: function get() { return this.props.suggest || this.props.freetext; } }]); return DropdownFilter; }(_dropdown2.default); DropdownFilter.propTypes = (0, _lodash.assign)({}, _dropdown2.default.propTypes, { /** * The ID value for the component * * @property value * @type {String} */ value: _propTypes2.default.oneOfType([_propTypes2.default.string, _propTypes2.default.number]), /** * The visible value for the component * Provides a visible value in `freetext` mode when no option is selected. * * @property visibleValue * @type {String} */ visibleValue: _propTypes2.default.string, /** * The options to be displayed in the dropdown. Should be set in the store and passed from the parent component. * * @property options * @type {object} */ options: _propTypes2.default.object.isRequired, /** * Enables create functionality for dropdown. * * @property create * @type {Function} */ create: _propTypes2.default.func, /** * Should the dropdown act and look like a suggestable input instead. * * @property suggest * @type {Boolean} */ suggest: _propTypes2.default.bool, /** * Should the dropdown accept free text as well as suggested options? * * @property freetext * @type {Boolean} */ freetext: _propTypes2.default.bool, /** * Name for freetext value hidden input containing visibleValue in freetext mode * * @property freetextName * @type {String} */ freetextName: _propTypes2.default.string }); exports.default = DropdownFilter;