thinkful-ui
Version:
Shared UI resources for Thinkful.
355 lines (305 loc) • 13.5 kB
JavaScript
;
function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a 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); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }
function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
var cx = require('classnames');
var escapeStringRegexp = require('escape-string-regexp');
var marked = require('marked');
var PropTypes = require('prop-types');
var React = require('react');
var Icon = require('../Icon');
var Tag = require('../Tag');
var DOWN_ARROW_KEY_CODE = 40;
var UP_ARROW_KEY_CODE = 38;
var COMMA_KEY_CODE = 188;
var RETURN_KEY_CODE = 13;
var DEFAULT_MIN_TOPIC_LENGTH = 1;
var mapLower = function mapLower(v) {
return v.toLowerCase();
};
/*
* TopicPicker component
*
* Interface:
* use the `getTopics` function on the component to get the internal
* list of picked topics
*
* @property {Array} availableTopics the Array of topic to suggest
* @property {Array} activeTopics the Array of topics to prefill
* @property {Boolean} addMatchEmphasis Add `em` tags around tag matches
* @property {String} className a className to add to component
* @property {Func} handleUpdateTopics callback func on topic update
* @property {Number} maxSuggestions The max number of suggestions to show
* @property {Number} minTopicLength The min length a topic string must be
*/
var TopicPicker = /*#__PURE__*/function (_React$Component) {
_inherits(TopicPicker, _React$Component);
var _super = _createSuper(TopicPicker);
function TopicPicker(props) {
var _this;
_classCallCheck(this, TopicPicker);
_this = _super.call(this, props); // Private methods
_this._filterTopicList = _this._filterTopicList.bind(_assertThisInitialized(_this));
_this._handleKeyDown = _this._handleKeyDown.bind(_assertThisInitialized(_this));
_this._handleMoveSelected = _this._handleMoveSelected.bind(_assertThisInitialized(_this));
_this._handlePatternChange = _this._handlePatternChange.bind(_assertThisInitialized(_this));
_this._handleAddTopic = _this._handleAddTopic.bind(_assertThisInitialized(_this));
_this._handleRemoveTopic = _this._handleRemoveTopic.bind(_assertThisInitialized(_this));
_this._handleTopicSubmit = _this._handleTopicSubmit.bind(_assertThisInitialized(_this));
_this._toggleFocus = _this._toggleFocus.bind(_assertThisInitialized(_this)); // Public methods
_this.getTopics = _this.getTopics.bind(_assertThisInitialized(_this));
_this.state = {
pattern: '',
topics: props.activeTopics,
selectedSuggestionIndex: -1
};
return _this;
}
_createClass(TopicPicker, [{
key: "componentWillReceiveProps",
value: function componentWillReceiveProps(newProps) {
this.setState({
topics: newProps.activeTopics
});
}
/*
* Return the `availableTopics`, filtered for a matching pattern
*
* This makes the topics markdown to add emphasis if the property
* `addMatchEmphasis` is truthy.
*/
}, {
key: "_filterTopicList",
value: function _filterTopicList(additionalTopics) {
var _this$props = this.props,
addMatchEmphasis = _this$props.addMatchEmphasis,
availableTopics = _this$props.availableTopics,
maxSuggestions = _this$props.maxSuggestions;
var _this$state = this.state,
pattern = _this$state.pattern,
topics = _this$state.topics; // in case topic is something like `C++`
var normalizedPattern = escapeStringRegexp(this.state.pattern.toLowerCase()); // find and mark the pattern matches in a case-insensitive way
return availableTopics // filter for matching available topics
.filter(function (topic) {
return topic.toLowerCase().match(normalizedPattern) && topics.indexOf(topic) < 0;
}) // limit the number of results
.slice(0, maxSuggestions) // add the asterisks for emphasis around matching area
.map(function (topic) {
if (addMatchEmphasis) {
var firstIndex = topic.toLowerCase().indexOf(topic.toLowerCase().match(normalizedPattern)[0]);
var lastIndex = firstIndex + pattern.length + 1;
var topicArray = topic.split('');
topicArray.splice(firstIndex, 0, '*');
topicArray.splice(lastIndex, 0, '*');
return topicArray.join('');
}
return topic;
});
}
}, {
key: "_handleKeyDown",
value: function _handleKeyDown(event) {
var selectedSuggestionIndex = this.state.selectedSuggestionIndex;
var suggestions = this._filterTopicList();
if (event.which === DOWN_ARROW_KEY_CODE) {
this.setState({
selectedSuggestionIndex: Math.min(selectedSuggestionIndex + 1, suggestions.length - 1)
});
} else if (event.which === UP_ARROW_KEY_CODE) {
this.setState({
selectedSuggestionIndex: Math.max(selectedSuggestionIndex - 1, 0)
});
} else if (event.which == COMMA_KEY_CODE || event.which == RETURN_KEY_CODE) {
this._handleTopicSubmit(event);
}
}
/*
* Move the selected suggestion by `numMoves`
*
* Usable in keydown handler for moving selected topic in suggestion list
*/
}, {
key: "_handleMoveSelected",
value: function _handleMoveSelected(numMoves) {
var availableTopics = this.props.availableTopics;
var selectedSuggestionIndex = this.state.selectedSuggestionIndex;
var newSelectedIndex = selectedSuggestionIndex + numMoves;
if (newSelectedIndex >= -1 && newSelectedIndex < (availableTopics || []).length) {
this.setState({
selectedSuggestionIndex: newSelectedIndex
});
}
}
/*
* Handler for a form change
*/
}, {
key: "_handlePatternChange",
value: function _handlePatternChange(event) {
this.setState({
pattern: event.target.value
});
}
/*
* Add a topic to the internal list of topics
*/
}, {
key: "_handleAddTopic",
value: function _handleAddTopic(topic) {
var topics = this.state.topics;
var handleUpdateTopics = this.props.handleUpdateTopics;
if (topics.map(mapLower).indexOf(topic.toLowerCase()) < 0) {
// We trim the whitespace off the tag before adding it
var newTopics = topics.concat(topic.trim());
this.setState({
topics: newTopics
});
handleUpdateTopics(newTopics);
}
this.setState({
pattern: '',
selectedSuggestionIndex: -1
});
}
/*
* Remove a topic from the internal list of topics, if it's present
*
* Matching is case-sensitive
*/
}, {
key: "_handleRemoveTopic",
value: function _handleRemoveTopic(topic) {
var topics = this.state.topics;
var handleUpdateTopics = this.props.handleUpdateTopics;
var newTopics = topics.filter(function (t) {
return t !== topic;
});
this.setState({
topics: newTopics
});
handleUpdateTopics(newTopics);
}
/*
* Handle submission of a new topic
*/
}, {
key: "_handleTopicSubmit",
value: function _handleTopicSubmit(event) {
if (event && event.preventDefault) {
event.preventDefault();
}
var selectedSuggestionIndex = this.state.selectedSuggestionIndex;
var topic;
if (selectedSuggestionIndex > -1) {
topic = this._filterTopicList()[selectedSuggestionIndex];
} else {
topic = this.state.pattern;
}
if (topic.trim().length >= this.props.minTopicLength) {
this._handleAddTopic(topic.trim());
}
}
}, {
key: "_toggleFocus",
value: function _toggleFocus() {
this.setState({
isFocused: !this.state.isFocused
});
}
/*
* Getter for topics
*/
}, {
key: "getTopics",
value: function getTopics() {
return this.state.topics;
}
}, {
key: "render",
value: function render() {
var _this2 = this;
var _this$props2 = this.props,
className = _this$props2.className,
placeholderText = _this$props2.placeholderText;
var _this$state2 = this.state,
isFocused = _this$state2.isFocused,
pattern = _this$state2.pattern,
selectedSuggestionIndex = _this$state2.selectedSuggestionIndex,
topics = _this$state2.topics;
return /*#__PURE__*/React.createElement("div", {
className: cx('topic-picker', className, isFocused && 'topic-picker-focus')
}, topics.map(function (topic, index) {
return /*#__PURE__*/React.createElement(Tag, {
key: index,
className: "topic",
displayName: topic,
forceEnabled: true
}, /*#__PURE__*/React.createElement("div", {
className: "topic-delete-button",
onClick: function onClick(event) {
return _this2._handleRemoveTopic(topic);
}
}, /*#__PURE__*/React.createElement(Icon, {
name: "close"
})));
}), /*#__PURE__*/React.createElement("div", {
className: "topic-form",
onKeyDown: this._handleKeyDown,
onSubmit: this._handleTopicSubmit
}, /*#__PURE__*/React.createElement("input", {
onFocus: this._toggleFocus,
onBlur: this._toggleFocus,
className: "topic-form-input",
type: "text",
value: pattern,
placeholder: placeholderText,
onChange: this._handlePatternChange
}), pattern && /*#__PURE__*/React.createElement("ul", {
className: "topic-suggestion-list"
}, this._filterTopicList().map(function (topic, index) {
return /*#__PURE__*/React.createElement("li", {
key: index,
className: cx('topic-list-item', {
selected: index === selectedSuggestionIndex
}),
onClick: function onClick(event) {
return _this2._handleAddTopic(topic.replace(/\*/g, ''));
},
dangerouslySetInnerHTML: {
__html: marked(topic)
}
});
}))));
}
}]);
return TopicPicker;
}(React.Component);
TopicPicker.propTypes = {
activeTopics: PropTypes.array,
availableTopics: PropTypes.array,
addMatchEmphasis: PropTypes.bool,
className: PropTypes.string,
handleUpdateTopics: PropTypes.func,
maxSuggestions: PropTypes.number,
minTopicLength: PropTypes.number
};
TopicPicker.defaultProps = {
availableTopics: [],
activeTopics: [],
maxSuggestions: 10,
minTopicLength: DEFAULT_MIN_TOPIC_LENGTH,
placeholderText: "Add a tag (hit 'return' after each one)",
// if parent doesn't pass in callback, to avoid conditionals inline
handleUpdateTopics: function handleUpdateTopics() {
return null;
}
};
module.exports = TopicPicker;