diff --git a/.gitignore b/.gitignore index 2459b1d..deeaf9e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -lib *.log node_modules .idea diff --git a/lib/MentionMenu.js b/lib/MentionMenu.js new file mode 100644 index 0000000..0704a6d --- /dev/null +++ b/lib/MentionMenu.js @@ -0,0 +1,117 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + +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 _react = require("react"); + +var _react2 = _interopRequireDefault(_react); + +var _reactPortalHoc = require("react-portal-hoc"); + +var _reactPortalHoc2 = _interopRequireDefault(_reactPortalHoc); + +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; } + +var MentionMenu = function (_React$Component) { + _inherits(MentionMenu, _React$Component); + + function MentionMenu(props) { + _classCallCheck(this, MentionMenu); + + var _this = _possibleConstructorReturn(this, (MentionMenu.__proto__ || Object.getPrototypeOf(MentionMenu)).call(this, props)); + + _this.state = { + left: props.left, + top: props.top + }; + return _this; + } + + _createClass(MentionMenu, [{ + key: "componentWillReceiveProps", + value: function componentWillReceiveProps(nextProps) { + var left = nextProps.left; + var top = nextProps.top; + + //prevent menu from going off the right of the screen + if (this.node && left + this.node.offsetWidth > window.innerWidth) { + left = window.innerWidth - (this.node.offsetWidth + 10); + } + //prevent menu from going off bottom of screen + if (this.node && top + this.node.offsetHeight > window.innerHeight) { + top = window.innerHeight - (this.node.offsetHeight + 10); + } + + if (left != this.state.left || top != this.state.top) { + this.setState({ left: left, top: top }); + } + } + }, { + key: "componentDidMount", + value: function componentDidMount() { + //prevent menu from going off the right of the screen + if (this.node && this.props.left + this.node.offsetWidth > window.innerWidth) { + this.setState({ left: window.innerWidth - (this.node.offsetWidth + 10) }); + } + //prevent menu from going off bottom of screen + if (this.node && this.props.top + this.node.offsetHeight > window.innerHeight) { + this.setState({ top: window.innerHeight - (this.node.offsetHeight + 10) }); + } + } + }, { + key: "render", + value: function render() { + var _this2 = this; + + var _props = this.props, + active = _props.active, + className = _props.className, + Item = _props.item, + options = _props.options, + hoverItem = _props.hoverItem, + selectItem = _props.selectItem, + _props$style = _props.style, + style = _props$style === undefined ? {} : _props$style; + var _state = this.state, + top = _state.top, + left = _state.left; + + + var menuStyle = _extends({}, style, { + left: left, + top: top, + position: "absolute" + }); + + return _react2.default.createElement( + "div", + { style: menuStyle, className: className, ref: function ref(node) { + return _this2.node = node; + } }, + options.map(function (option, idx) { + return _react2.default.createElement( + "div", + { key: idx, onClick: selectItem(idx), onMouseOver: hoverItem(idx) }, + _react2.default.createElement(Item, _extends({ active: active === idx }, option)) + ); + }) + ); + } + }]); + + return MentionMenu; +}(_react2.default.Component); + +exports.default = (0, _reactPortalHoc2.default)({ clickToClose: true, escToClose: true })(MentionMenu); \ No newline at end of file diff --git a/lib/MentionWrapper.js b/lib/MentionWrapper.js new file mode 100644 index 0000000..4bf34ca --- /dev/null +++ b/lib/MentionWrapper.js @@ -0,0 +1,335 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + +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 _react = require("react"); + +var _react2 = _interopRequireDefault(_react); + +var _textareaCaret = require("textarea-caret"); + +var _textareaCaret2 = _interopRequireDefault(_textareaCaret); + +var _propTypes = require("prop-types"); + +var _propTypes2 = _interopRequireDefault(_propTypes); + +var _MentionMenu = require("./MentionMenu"); + +var _MentionMenu2 = _interopRequireDefault(_MentionMenu); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } + +function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } + +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; } + +var getMenuProps = function getMenuProps(keystrokeTriggered, children) { + var child = Array.isArray(children) ? children[keystrokeTriggered] : children; + return child ? child.props : {}; +}; + +var defaultReplace = function defaultReplace(userObj, trigger) { + return "" + trigger + userObj.value + " "; +}; + +var MentionWrapper = function (_Component) { + _inherits(MentionWrapper, _Component); + + function MentionWrapper(props) { + var _this2 = this; + + _classCallCheck(this, MentionWrapper); + + var _this = _possibleConstructorReturn(this, (MentionWrapper.__proto__ || Object.getPrototypeOf(MentionWrapper)).call(this, props)); + + _this.makeOptions = function () { + var _ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee(query, resolve) { + var options; + return regeneratorRuntime.wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + _context.next = 2; + return resolve(query); + + case 2: + options = _context.sent; + + if (options.length > 0) { + _this.setState({ + options: options + }); + } else { + _this.closeMenu(); + } + + case 4: + case "end": + return _context.stop(); + } + } + }, _callee, _this2); + })); + + return function (_x, _x2) { + return _ref.apply(this, arguments); + }; + }(); + + _this.handleInput = function (e) { + _this.maybeMention(); + var onInput = _this.props.onInput; + + if (onInput) { + onInput(e); + } + }; + + _this.inputRef = function (c) { + _this.ref = c; + var getRef = _this.props.getRef; + + if (getRef) { + getRef(c); + } + }; + + _this.handleBlur = function (e) { + // if the menu is open, don't treat a click as a blur (required for the click handler) + var onBlur = _this.props.onBlur; + + if (onBlur && !_this.state.top) { + onBlur(e); + } + }; + + _this.handleKeyDown = function (e) { + var _this$state = _this.state, + options = _this$state.options, + active = _this$state.active, + triggerIdx = _this$state.triggerIdx; + + var keyCaught = void 0; + if (triggerIdx !== undefined) { + if (e.key === "ArrowDown") { + _this.setState({ + active: Math.min(active + 1, options.length - 1) + }); + keyCaught = true; + } else if (e.key === "ArrowUp") { + _this.setState({ + active: Math.max(active - 1, 0) + }); + keyCaught = true; + } else if (e.key === "Tab" || e.key === "Enter") { + _this.selectItem(active)(e); + keyCaught = true; + } + } + var onKeyDown = _this.props.onKeyDown; + + if (keyCaught) { + e.preventDefault(); + } else if (onKeyDown) { + // only call the passed in keyDown handler if the key wasn't one of ours + onKeyDown(e); + } + }; + + _this.selectItem = function (active) { + return function (e) { + var _this$state2 = _this.state, + options = _this$state2.options, + triggerIdx = _this$state2.triggerIdx; + + var preMention = _this.ref.value.substr(0, triggerIdx); + var option = options[active]; + var mention = _this.replace(option, _this.ref.value[triggerIdx]); + var postMention = _this.ref.value.substr(_this.ref.selectionStart); + var newValue = "" + preMention + mention + postMention; + _this.ref.value = newValue; + var onChange = _this.props.onChange; + + if (onChange) { + onChange(e, newValue); + } + var caretPosition = _this.ref.value.length - postMention.length; + _this.ref.setSelectionRange(caretPosition, caretPosition); + _this.closeMenu(); + _this.ref.focus(); + }; + }; + + _this.setActiveOnEvent = function (active) { + return function (e) { + _this.setState({ + active: active + }); + }; + }; + + _this.state = { + child: {}, + options: [] + }; + var children = props.children; + + _this.triggers = _react.Children.map(children, function (child) { + return child.props.trigger; + }); + _this.closeMenu = _this.closeMenu.bind(_this); + return _this; + } + + _createClass(MentionWrapper, [{ + key: "maybeMention", + value: function maybeMention() { + var _this3 = this; + + // get the text preceding the cursor position + var textBeforeCaret = this.ref.value.slice(0, this.ref.selectionStart); + + // split string by whitespaces and get the last word (where the cursor currently stands) + var tokens = textBeforeCaret.split(/\s/); + var lastToken = tokens[tokens.length - 1]; + + // check if the text befor the caret ends with the last word + var triggerIdx = textBeforeCaret.endsWith(lastToken) ? textBeforeCaret.length - lastToken.length : -1; + // and if that last word starts with a trigger + var maybeTrigger = textBeforeCaret[triggerIdx]; + var keystrokeTriggered = this.triggers.indexOf(maybeTrigger); + + if (keystrokeTriggered !== -1) { + (function () { + var positionIndex = _this3.ref.selectionStart; + if (_this3.props.position === "start") { + positionIndex = triggerIdx + 1; + } + var query = textBeforeCaret.slice(triggerIdx + 1); + var coords = (0, _textareaCaret2.default)(_this3.ref, positionIndex); + + var _ref$getBoundingClien = _this3.ref.getBoundingClientRect(), + top = _ref$getBoundingClien.top, + left = _ref$getBoundingClien.left; + + var child = getMenuProps(keystrokeTriggered, _this3.props.children); + var replace = child.replace, + resolve = child.resolve; + + _this3.replace = replace || defaultReplace; + _this3.makeOptions(query, resolve); + // that stupid bug where the caret moves to the end happens unless we do it next tick + setTimeout(function () { + _this3.setState({ + active: 0, + child: child, + left: window.pageXOffset + coords.left + left + _this3.ref.scrollLeft, + triggerIdx: triggerIdx, + top: (window.pageYOffset || 0) + coords.top + top + coords.height - _this3.ref.scrollTop + }); + }, 0); + })(); + } else { + this.closeMenu(); + } + } + }, { + key: "closeMenu", + value: function closeMenu() { + var _this4 = this; + + setTimeout(function () { + _this4.setState({ + child: {}, + options: [], + left: undefined, + top: undefined, + triggerIdx: undefined + }); + }, 0); + } + }, { + key: "render", + value: function render() { + var _props = this.props, + children = _props.children, + component = _props.component, + CustomComponent = _props.CustomComponent, + getRef = _props.getRef, + containerStyle = _props.containerStyle, + textWrapperClassName = _props.textWrapperClassName, + inputProps = _objectWithoutProperties(_props, ["children", "component", "CustomComponent", "getRef", "containerStyle", "textWrapperClassName"]); + + var _state = this.state, + active = _state.active, + child = _state.child, + left = _state.left, + top = _state.top, + options = _state.options; + var item = child.item, + className = child.className, + style = child.style; + + + return _react2.default.createElement( + "div", + { className: textWrapperClassName, style: containerStyle }, + _react2.default.createElement("textarea", _extends({}, inputProps, { + ref: this.inputRef, + onBlur: this.handleBlur, + onInput: this.handleInput, + onKeyDown: this.handleKeyDown + })), + CustomComponent ? top !== undefined && _react2.default.createElement(CustomComponent, { + active: active, + className: className, + closeFunc: this.closeMenu, + left: left, + isOpen: options.length > 0, + item: item, + options: options, + hoverItem: this.setActiveOnEvent, + selectItem: this.selectItem, + style: style, + top: top + }) : top !== undefined && _react2.default.createElement(_MentionMenu2.default, { + active: active, + className: className, + left: left, + isOpen: options.length > 0, + item: item, + options: options, + hoverItem: this.setActiveOnEvent, + selectItem: this.selectItem, + style: style, + top: top + }) + ); + } + }]); + + return MentionWrapper; +}(_react.Component); + +MentionWrapper.propTypes = { + position: _propTypes2.default.oneOf(["start", "caret"]) +}; + +MentionWrapper.defaultProps = { + position: "caret" +}; + +exports.default = MentionWrapper; \ No newline at end of file diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..71520a1 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,19 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.MentionMenu = exports.MentionWrapper = undefined; + +var _MentionWrapper2 = require("./MentionWrapper"); + +var _MentionWrapper3 = _interopRequireDefault(_MentionWrapper2); + +var _MentionMenu2 = require("./MentionMenu"); + +var _MentionMenu3 = _interopRequireDefault(_MentionMenu2); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +exports.MentionWrapper = _MentionWrapper3.default; +exports.MentionMenu = _MentionMenu3.default; \ No newline at end of file diff --git a/package.json b/package.json index f4035ae..6e2aa36 100644 --- a/package.json +++ b/package.json @@ -35,11 +35,13 @@ "rimraf": "^2.5.3" }, "dependencies": { + "react": "^16.4.1", + "react-dom": "^16.4.1", "react-portal-hoc": "^0.4.1", "textarea-caret": "git://github.com/mattkrick/textarea-caret-position.git#ede461f40712238c505e4a1861fc68de6e7731ad" }, "peerDependencies": { - "react": "^15.4.2", - "react-dom": "^15.4.2" + "react": "^16.4.1", + "react-dom": "^16.4.1" } } diff --git a/src/MentionMenu.js b/src/MentionMenu.js index c80eb1b..1e31a07 100644 --- a/src/MentionMenu.js +++ b/src/MentionMenu.js @@ -1,35 +1,76 @@ import React from "react"; import portal from "react-portal-hoc"; -const MentionMenu = props => { - const { - active, - className, - item: Item, - options, - top, - left, - hoverItem, - selectItem, - style = {} - } = props; - const menuStyle = { - ...style, - left, - top, - position: "absolute" - }; - return ( -
- {options.map((option, idx) => { - return ( -
- -
- ); - })} -
- ); -}; +class MentionMenu extends React.Component { + constructor(props) { + super(props); + this.state = { + left: props.left, + top: props.top + }; + } + componentWillReceiveProps(nextProps) { + let left = nextProps.left; + let top = nextProps.top; + + //prevent menu from going off the right of the screen + if (this.node && left + this.node.offsetWidth > window.innerWidth) { + left = window.innerWidth - (this.node.offsetWidth + 10); + } + //prevent menu from going off bottom of screen + if (this.node && top + this.node.offsetHeight > window.innerHeight) { + top = window.innerHeight - (this.node.offsetHeight + 10); + } + + if (left != this.state.left || top != this.state.top) { + this.setState({ left, top }); + } + } + componentDidMount() { + //prevent menu from going off the right of the screen + if (this.node && this.props.left + this.node.offsetWidth > window.innerWidth) { + this.setState({ left: window.innerWidth - (this.node.offsetWidth + 10) }); + } + //prevent menu from going off bottom of screen + if (this.node && this.props.top + this.node.offsetHeight > window.innerHeight) { + this.setState({ top: window.innerHeight - (this.node.offsetHeight + 10) }); + } + } + render() { + const { + active, + className, + item: Item, + options, + hoverItem, + selectItem, + style = {} + } = this.props; + + const { + top, + left + } = this.state; + + const menuStyle = { + ...style, + left, + top, + position: "absolute" + }; + + return ( +
this.node = node}> + {options.map((option, idx) => { + return ( +
+ +
+ ); + })} +
+ ); + } +} export default portal({ clickToClose: true, escToClose: true })(MentionMenu); diff --git a/src/MentionWrapper.js b/src/MentionWrapper.js index 1ddaeef..6e6085a 100644 --- a/src/MentionWrapper.js +++ b/src/MentionWrapper.js @@ -21,6 +21,7 @@ class MentionWrapper extends Component { }; const { children } = props; this.triggers = Children.map(children, child => child.props.trigger); + this.closeMenu = this.closeMenu.bind(this); } makeOptions = async (query, resolve) => { @@ -67,10 +68,10 @@ class MentionWrapper extends Component { this.setState({ active: 0, child, - left: window.scrollX + coords.left + left + this.ref.scrollLeft, + left: window.pageXOffset + coords.left + left + this.ref.scrollLeft, triggerIdx, top: - window.scrollY + + (window.pageYOffset || 0) + coords.top + top + coords.height - @@ -171,11 +172,20 @@ class MentionWrapper extends Component { } render() { - const { children, component, getRef, ...inputProps } = this.props; + const { + children, + component, + CustomComponent, + getRef, + containerStyle, + textWrapperClassName, + ...inputProps + } = this.props; const { active, child, left, top, options } = this.state; const { item, className, style } = child; + return ( -
+