From 506025aee28a2c4121335408c744b7225dcd3691 Mon Sep 17 00:00:00 2001 From: Ashley Maberry Date: Wed, 14 Feb 2018 16:10:04 -0700 Subject: [PATCH 01/17] fix IE11 issues and fix so menu doesnt go off right side of screen or bottom --- .gitignore | 1 - lib/MentionMenu.js | 116 +++++++++++++++ lib/MentionWrapper.js | 318 ++++++++++++++++++++++++++++++++++++++++++ lib/index.js | 19 +++ package.json | 1 + src/MentionMenu.js | 100 +++++++++---- src/MentionWrapper.js | 4 +- yarn.lock | 87 +++++++++++- 8 files changed, 611 insertions(+), 35 deletions(-) create mode 100644 lib/MentionMenu.js create mode 100644 lib/MentionWrapper.js create mode 100644 lib/index.js 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..0e025ac --- /dev/null +++ b/lib/MentionMenu.js @@ -0,0 +1,116 @@ +"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, right: right }); + } + } + }, { + 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..c6230c4 --- /dev/null +++ b/lib/MentionWrapper.js @@ -0,0 +1,318 @@ +"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; + }); + 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, + getRef = _props.getRef, + inputProps = _objectWithoutProperties(_props, ["children", "component", "getRef"]); + + 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", + null, + _react2.default.createElement("textarea", _extends({}, inputProps, { + ref: this.inputRef, + onBlur: this.handleBlur, + onInput: this.handleInput, + onKeyDown: this.handleKeyDown + })), + 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..a9d7cb4 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "rimraf": "^2.5.3" }, "dependencies": { + "react": "^16.2.0", "react-portal-hoc": "^0.4.1", "textarea-caret": "git://github.com/mattkrick/textarea-caret-position.git#ede461f40712238c505e4a1861fc68de6e7731ad" }, diff --git a/src/MentionMenu.js b/src/MentionMenu.js index c80eb1b..21f06f1 100644 --- a/src/MentionMenu.js +++ b/src/MentionMenu.js @@ -1,35 +1,75 @@ 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, right }); + } + } + 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..288aca5 100644 --- a/src/MentionWrapper.js +++ b/src/MentionWrapper.js @@ -67,10 +67,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 - diff --git a/yarn.lock b/yarn.lock index 71d7bad..05cb93a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -50,6 +50,10 @@ arrify@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" +asap@~2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + asn1@~0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" @@ -875,6 +879,10 @@ convert-source-map@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.3.0.tgz#e9f3e9c6e2728efc2676696a70eb382f73106a67" +core-js@^1.0.0: + version "1.2.7" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" + core-js@^2.4.0: version "2.4.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" @@ -931,6 +939,12 @@ ecc-jsbn@~0.1.1: dependencies: jsbn "~0.1.0" +encoding@^0.1.11: + version "0.1.12" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" + dependencies: + iconv-lite "~0.4.13" + escape-string-regexp@^1.0.2: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -965,6 +979,18 @@ extsprintf@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550" +fbjs@^0.8.16: + version "0.8.16" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db" + dependencies: + core-js "^1.0.0" + isomorphic-fetch "^2.1.1" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + setimmediate "^1.0.5" + ua-parser-js "^0.7.9" + filename-regex@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.0.tgz#996e3e80479b98b9897f15a8a58b3d084e926775" @@ -1146,6 +1172,10 @@ http-signature@~1.1.0: jsprim "^1.2.2" sshpk "^1.7.0" +iconv-lite@~0.4.13: + version "0.4.19" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -1240,6 +1270,10 @@ is-property@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" +is-stream@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -1254,6 +1288,13 @@ isobject@^2.0.0: dependencies: isarray "1.0.0" +isomorphic-fetch@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" + dependencies: + node-fetch "^1.0.1" + whatwg-fetch ">=0.10.0" + isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -1314,7 +1355,7 @@ lodash@^4.2.0: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" -loose-envify@^1.0.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" dependencies: @@ -1380,6 +1421,13 @@ nan@^2.3.0: version "2.5.1" resolved "https://registry.yarnpkg.com/nan/-/nan-2.5.1.tgz#d5b01691253326a97a2bbee9e61c55d8d60351e2" +node-fetch@^1.0.1: + version "1.7.3" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" + dependencies: + encoding "^0.1.11" + is-stream "^1.0.1" + node-pre-gyp@^0.6.29: version "0.6.32" resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.32.tgz#fc452b376e7319b3d255f5f34853ef6fd8fe1fd5" @@ -1421,7 +1469,7 @@ oauth-sign@~0.8.1: version "0.8.2" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" -object-assign@^4.1.0: +object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -1495,6 +1543,20 @@ process-nextick-args@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" +promise@^7.1.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" + dependencies: + asap "~2.0.3" + +prop-types@^15.6.0: + version "15.6.0" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856" + dependencies: + fbjs "^0.8.16" + loose-envify "^1.3.1" + object-assign "^4.1.1" + punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" @@ -1523,6 +1585,15 @@ react-portal-hoc@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/react-portal-hoc/-/react-portal-hoc-0.4.1.tgz#c1e31b571bc227f936f57c766223e0e60dfe1cfa" +react@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba" + dependencies: + fbjs "^0.8.16" + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.0" + "readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.0.2: version "2.2.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.2.tgz#a9e6fec3c7dda85f8bb1b3ba7028604556fc825e" @@ -1654,6 +1725,10 @@ set-immediate-shim@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + signal-exit@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" @@ -1770,6 +1845,10 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" +ua-parser-js@^0.7.9: + version "0.7.17" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac" + uid-number@~0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" @@ -1798,6 +1877,10 @@ verror@1.3.6: dependencies: extsprintf "1.0.2" +whatwg-fetch@>=0.10.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84" + wide-align@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.0.tgz#40edde802a71fea1f070da3e62dcda2e7add96ad" From ace45890aeeb7f8c54f92a7c83a27c5435ee0e99 Mon Sep 17 00:00:00 2001 From: Ashley Maberry Date: Fri, 16 Feb 2018 09:25:46 -0700 Subject: [PATCH 02/17] use 'top' instead of 'right' --- lib/MentionMenu.js | 2 +- src/MentionMenu.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/MentionMenu.js b/lib/MentionMenu.js index 0e025ac..d6b16c4 100644 --- a/lib/MentionMenu.js +++ b/lib/MentionMenu.js @@ -55,7 +55,7 @@ var MentionMenu = function (_React$Component) { } if (left != this.state.left || top != this.state.top) { - this.setState({ left: left, right: right }); + this.setState({ left: left, top: top }); } } }, { diff --git a/src/MentionMenu.js b/src/MentionMenu.js index 21f06f1..9e27918 100644 --- a/src/MentionMenu.js +++ b/src/MentionMenu.js @@ -23,7 +23,7 @@ class MentionMenu extends React.Component { } if (left != this.state.left || top != this.state.top) { - this.setState({ left, right }); + this.setState({ left, top }); } } componentDidMount() { From 68e3bc2f481a6d57924502d1f6319f30b85e8d9d Mon Sep 17 00:00:00 2001 From: Ryan Yiu Date: Wed, 20 Jun 2018 12:20:20 -0700 Subject: [PATCH 03/17] Updated dependencies - Updated react and react-dom dependencies to the current versions - Updated package.json to include the current versions (also added peer dependencies to dependencies because there was an issue of yarn build not adding the required node_modules properly) --- package.json | 7 ++++--- yarn.lock | 15 ++++++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index a9d7cb4..6e2aa36 100644 --- a/package.json +++ b/package.json @@ -35,12 +35,13 @@ "rimraf": "^2.5.3" }, "dependencies": { - "react": "^16.2.0", + "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/yarn.lock b/yarn.lock index 05cb93a..54d0baa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1581,13 +1581,22 @@ rc@~1.1.6: minimist "^1.2.0" strip-json-comments "~1.0.4" +react-dom@^16.4.1: + version "16.4.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.4.1.tgz#7f8b0223b3a5fbe205116c56deb85de32685dad6" + dependencies: + fbjs "^0.8.16" + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.0" + react-portal-hoc@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/react-portal-hoc/-/react-portal-hoc-0.4.1.tgz#c1e31b571bc227f936f57c766223e0e60dfe1cfa" -react@^16.2.0: - version "16.2.0" - resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba" +react@^16.4.1: + version "16.4.1" + resolved "https://registry.yarnpkg.com/react/-/react-16.4.1.tgz#de51ba5764b5dbcd1f9079037b862bd26b82fe32" dependencies: fbjs "^0.8.16" loose-envify "^1.1.0" From 700ed12f0839f29aae3843ba0f76f8ed4c01f2f8 Mon Sep 17 00:00:00 2001 From: Ryan Yiu Date: Tue, 26 Jun 2018 10:25:47 -0700 Subject: [PATCH 04/17] Fixed mention portal on mobile --- lib/MentionMenu.js | 6 +-- lib/MentionPortal.js | 96 +++++++++++++++++++++++++++++++++++++++++++ lib/MentionWrapper.js | 39 ++++++++++++------ src/MentionMenu.js | 3 +- src/MentionPortal.js | 52 +++++++++++++++++++++++ src/MentionWrapper.js | 33 +++++++++------ 6 files changed, 196 insertions(+), 33 deletions(-) create mode 100644 lib/MentionPortal.js create mode 100644 src/MentionPortal.js diff --git a/lib/MentionMenu.js b/lib/MentionMenu.js index d6b16c4..1d17e4d 100644 --- a/lib/MentionMenu.js +++ b/lib/MentionMenu.js @@ -12,10 +12,6 @@ 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"); } } @@ -113,4 +109,4 @@ var MentionMenu = function (_React$Component) { return MentionMenu; }(_react2.default.Component); -exports.default = (0, _reactPortalHoc2.default)({ clickToClose: true, escToClose: true })(MentionMenu); \ No newline at end of file +exports.default = MentionMenu; \ No newline at end of file diff --git a/lib/MentionPortal.js b/lib/MentionPortal.js new file mode 100644 index 0000000..2135a87 --- /dev/null +++ b/lib/MentionPortal.js @@ -0,0 +1,96 @@ +"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 _react = require("react"); + +var _react2 = _interopRequireDefault(_react); + +var _reactDom = require("react-dom"); + +var _reactDom2 = _interopRequireDefault(_reactDom); + +var _propTypes = require("prop-types"); + +var _propTypes2 = _interopRequireDefault(_propTypes); + +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 MentionPortal = function (_Component) { + _inherits(MentionPortal, _Component); + + function MentionPortal() { + var _ref; + + var _temp, _this, _ret; + + _classCallCheck(this, MentionPortal); + + for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref = MentionPortal.__proto__ || Object.getPrototypeOf(MentionPortal)).call.apply(_ref, [this].concat(args))), _this), _this.onClickFunc = function (event) { + _this.props.closeOnClick && _this.props.closeFunc(); + }, _this.onEscFunc = function (event) { + if (_this.props.closeOnEsc && event.keyCode === 27) { + _this.props.closeFunc(); + } + }, _temp), _possibleConstructorReturn(_this, _ret); + } + + _createClass(MentionPortal, [{ + key: "componentDidMount", + value: function componentDidMount() { + if (document) { + document.addEventListener("click", this.onClickFunc); + document.addEventListener("keyup", this.onEscFunc); + } + } + }, { + key: "componentWillMount", + value: function componentWillMount() { + this.portal = document.createElement("div"); + document.body.appendChild(this.portal); + } + }, { + key: "componentWillUnmount", + value: function componentWillUnmount() { + if (document) { + document.removeEventListener("click", this.onClickFunc); + document.removeEventListener("keyup", this.onEscFunc); + } + document.body.removeChild(this.portal); + } + }, { + key: "render", + value: function render() { + return _reactDom2.default.createPortal(this.props.children, this.portal); + } + }]); + + return MentionPortal; +}(_react.Component); + +MentionPortal.propTypes = { + closeFunc: _propTypes2.default.func.isRequired, + closeOnClick: _propTypes2.default.bool.isRequired, + closeOnEsc: _propTypes2.default.bool.isRequired +}; + +MentionPortal.defaultProps = { + closeOnClick: true, + closeOnEsc: true +}; + +exports.default = MentionPortal; \ No newline at end of file diff --git a/lib/MentionWrapper.js b/lib/MentionWrapper.js index c6230c4..0199ddc 100644 --- a/lib/MentionWrapper.js +++ b/lib/MentionWrapper.js @@ -24,6 +24,10 @@ var _MentionMenu = require("./MentionMenu"); var _MentionMenu2 = _interopRequireDefault(_MentionMenu); +var _MentionPortal = require("./MentionPortal"); + +var _MentionPortal2 = _interopRequireDefault(_MentionPortal); + 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; } @@ -190,6 +194,7 @@ var MentionWrapper = function (_Component) { _this.triggers = _react.Children.map(children, function (child) { return child.props.trigger; }); + _this.closeMenu = _this.closeMenu.bind(_this); return _this; } @@ -267,7 +272,8 @@ var MentionWrapper = function (_Component) { children = _props.children, component = _props.component, getRef = _props.getRef, - inputProps = _objectWithoutProperties(_props, ["children", "component", "getRef"]); + portal = _props.portal, + inputProps = _objectWithoutProperties(_props, ["children", "component", "getRef", "portal"]); var _state = this.state, active = _state.active, @@ -279,6 +285,7 @@ var MentionWrapper = function (_Component) { className = child.className, style = child.style; + var Portal = this.props.portal; return _react2.default.createElement( "div", null, @@ -288,18 +295,22 @@ var MentionWrapper = function (_Component) { onInput: this.handleInput, onKeyDown: this.handleKeyDown })), - 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 - }) + top !== undefined && _react2.default.createElement( + Portal, + { closeFunc: this.closeMenu }, + _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 + }) + ) ); } }]); @@ -308,10 +319,12 @@ var MentionWrapper = function (_Component) { }(_react.Component); MentionWrapper.propTypes = { + portal: _propTypes2.default.func.isRequired, position: _propTypes2.default.oneOf(["start", "caret"]) }; MentionWrapper.defaultProps = { + portal: _MentionPortal2.default, position: "caret" }; diff --git a/src/MentionMenu.js b/src/MentionMenu.js index 9e27918..c2da59f 100644 --- a/src/MentionMenu.js +++ b/src/MentionMenu.js @@ -1,5 +1,4 @@ import React from "react"; -import portal from "react-portal-hoc"; class MentionMenu extends React.Component { constructor(props) { @@ -72,4 +71,4 @@ class MentionMenu extends React.Component { } } -export default portal({ clickToClose: true, escToClose: true })(MentionMenu); +export default MentionMenu; diff --git a/src/MentionPortal.js b/src/MentionPortal.js new file mode 100644 index 0000000..0bbeefc --- /dev/null +++ b/src/MentionPortal.js @@ -0,0 +1,52 @@ +import React, { Component } from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; + +class MentionPortal extends Component { + onClickFunc = event => { + this.props.closeOnClick && this.props.closeFunc(); + } + + onEscFunc = event => { + if (this.props.closeOnEsc && event.keyCode === 27) { + this.props.closeFunc(); + } + } + + componentDidMount() { + if (document) { + document.addEventListener("click", this.onClickFunc); + document.addEventListener("keyup", this.onEscFunc); + } + } + + componentWillMount() { + this.portal = document.createElement("div"); + document.body.appendChild(this.portal); + } + + componentWillUnmount() { + if (document) { + document.removeEventListener("click", this.onClickFunc); + document.removeEventListener("keyup", this.onEscFunc); + } + document.body.removeChild(this.portal); + } + + render() { + return ReactDOM.createPortal(this.props.children, this.portal); + } +} + +MentionPortal.propTypes = { + closeFunc: PropTypes.func.isRequired, + closeOnClick: PropTypes.bool.isRequired, + closeOnEsc: PropTypes.bool.isRequired +}; + +MentionPortal.defaultProps = { + closeOnClick: true, + closeOnEsc: true +} + +export default MentionPortal; \ No newline at end of file diff --git a/src/MentionWrapper.js b/src/MentionWrapper.js index 288aca5..6737d49 100644 --- a/src/MentionWrapper.js +++ b/src/MentionWrapper.js @@ -2,6 +2,7 @@ import React, { Component, Children } from "react"; import getCaretCoords from "textarea-caret"; import PropTypes from "prop-types"; import MentionMenu from "./MentionMenu"; +import MentionPortal from "./MentionPortal"; const getMenuProps = (keystrokeTriggered, children) => { const child = Array.isArray(children) @@ -21,6 +22,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) => { @@ -171,9 +173,10 @@ class MentionWrapper extends Component { } render() { - const { children, component, getRef, ...inputProps } = this.props; + const { children, component, getRef, portal, ...inputProps } = this.props; const { active, child, left, top, options } = this.state; const { item, className, style } = child; + const Portal = this.props.portal; return (