react-mde
Version:
React Markdown Editor
306 lines (305 loc) • 16.2 kB
JavaScript
;
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TextArea = void 0;
var React = require("react");
var ClassNames_1 = require("../util/ClassNames");
var TextAreaCaretPosition_1 = require("../util/TextAreaCaretPosition");
var InsertTextAtPosition_1 = require("../util/InsertTextAtPosition");
var Math_1 = require("../util/Math");
var SuggestionsDropdown_1 = require("./SuggestionsDropdown");
var TextArea = /** @class */ (function (_super) {
__extends(TextArea, _super);
function TextArea(props) {
var _this = _super.call(this, props) || this;
_this.currentLoadSuggestionsPromise = Promise.resolve(undefined);
/**
* suggestionsPromiseIndex exists as a means to cancel what happens when the suggestions promise finishes loading.
*
* When the user is searching for suggestions, there is a promise that, when resolved, causes a re-render.
* However, in case there is another promise to be resolved after the current one, it does not make sense to re-render
* only to re-render again after the next one is complete.
*
* When there is a promise loading and the user cancels the suggestion, you don't want the status to go back to "active"
* when the promise resolves.
*
* suggestionsPromiseIndex increments every time the mentions query
*/
_this.suggestionsPromiseIndex = 0;
_this.getTextArea = function () {
return _this.props.refObject.current;
};
_this.handleOnChange = function (event) {
var onChange = _this.props.onChange;
onChange(event.target.value);
};
_this.handleBlur = function () {
var mention = _this.state.mention;
if (mention) {
_this.setState({ mention: { status: "inactive", suggestions: [] } });
}
};
_this.startLoadingSuggestions = function (text) {
var promiseIndex = ++_this.suggestionsPromiseIndex;
var loadSuggestions = _this.props.loadSuggestions;
_this.currentLoadSuggestionsPromise = _this.currentLoadSuggestionsPromise
.then(function () { return loadSuggestions(text, _this.state.mention.triggeredBy); })
.then(function (suggestions) {
if (_this.state.mention.status === "inactive") {
// This means this promise resolved too late when the status has already been set to inactice
return;
}
else if (_this.suggestionsPromiseIndex === promiseIndex) {
if (!suggestions || !suggestions.length) {
_this.setState({
mention: {
status: "inactive",
suggestions: []
}
});
}
else {
_this.setState({
mention: __assign(__assign({}, _this.state.mention), { status: "active", suggestions: suggestions, focusIndex: 0 })
});
}
_this.suggestionsPromiseIndex = 0;
}
return Promise.resolve();
});
};
_this.loadEmptySuggestion = function (target, key) {
var caret = TextAreaCaretPosition_1.getCaretCoordinates(target, key);
_this.startLoadingSuggestions("");
_this.setState({
mention: {
status: "loading",
startPosition: target.selectionStart + 1,
caret: caret,
suggestions: [],
triggeredBy: key
}
});
};
_this.handleSuggestionSelected = function (index) {
var mention = _this.state.mention;
_this.getTextArea().selectionStart = mention.startPosition - 1;
var textForInsert = _this.props.value.substr(_this.getTextArea().selectionStart, _this.getTextArea().selectionEnd - _this.getTextArea().selectionStart);
InsertTextAtPosition_1.insertText(_this.getTextArea(), mention.suggestions[index].value + " ");
_this.setState({
mention: {
status: "inactive",
suggestions: []
}
});
};
_this.handleKeyDown = function (event) {
if (_this.props.onPossibleKeyCommand) {
var handled = _this.props.onPossibleKeyCommand(event);
if (handled) {
event.preventDefault();
// If the keydown resulted in a command being executed, we will just close the suggestions if they are open.
// Resetting suggestionsPromiseIndex will cause any promise that is yet to be resolved to have no effect
// when they finish loading.
// TODO: The code below is duplicate, we need to clean this up
_this.suggestionsPromiseIndex = 0;
_this.setState({
mention: {
status: "inactive",
suggestions: []
}
});
return;
}
}
if (!_this.suggestionsEnabled()) {
return;
}
var key = event.key, shiftKey = event.shiftKey, currentTarget = event.currentTarget;
var selectionStart = currentTarget.selectionStart;
var mention = _this.state.mention;
switch (mention.status) {
case "loading":
case "active":
if (key === "Escape" ||
(key === "Backspace" &&
selectionStart <= _this.state.mention.startPosition)) {
// resetting suggestionsPromiseIndex will cause any promise that is yet to be resolved to have no effect
// when they finish loading.
_this.suggestionsPromiseIndex = 0;
_this.setState({
mention: {
status: "inactive",
suggestions: []
}
});
}
else if (mention.status === "active" &&
(key === "ArrowUp" || key === "ArrowDown") &&
!shiftKey) {
event.preventDefault();
var focusDelta = key === "ArrowUp" ? -1 : 1;
_this.setState({
mention: __assign(__assign({}, mention), { focusIndex: Math_1.mod(mention.focusIndex + focusDelta, mention.suggestions.length) })
});
}
else if (key === "Enter" &&
mention.status === "active" &&
mention.suggestions.length) {
event.preventDefault();
_this.handleSuggestionSelected(mention.focusIndex);
}
break;
default:
// Ignore
}
};
_this.handleKeyUp = function (event) {
var key = event.key;
var mention = _this.state.mention;
var _a = _this.props, suggestionTriggerCharacters = _a.suggestionTriggerCharacters, value = _a.value;
switch (mention.status) {
case "loading":
case "active":
if (key === "Backspace") {
var searchText = value.substr(mention.startPosition, _this.getTextArea().selectionStart - mention.startPosition);
_this.startLoadingSuggestions(searchText);
if (mention.status !== "loading") {
_this.setState({
mention: __assign(__assign({}, _this.state.mention), { status: "loading" })
});
}
}
break;
case "inactive":
if (key === "Backspace") {
var prevChar = value.charAt(_this.getTextArea().selectionStart - 1);
var isAtMention = suggestionTriggerCharacters.includes(value.charAt(_this.getTextArea().selectionStart - 1));
if (isAtMention) {
_this.loadEmptySuggestion(event.currentTarget, prevChar);
}
}
break;
default:
// Ignore
}
};
_this.handleKeyPress = function (event) {
var _a = _this.props, suggestionTriggerCharacters = _a.suggestionTriggerCharacters, value = _a.value;
var mention = _this.state.mention;
var key = event.key;
switch (mention.status) {
case "loading":
case "active":
if (key === " ") {
_this.setState({
mention: __assign(__assign({}, _this.state.mention), { status: "inactive" })
});
return;
}
var searchText = value.substr(mention.startPosition, _this.getTextArea().selectionStart - mention.startPosition) + key;
// In this case, the mentions box was open but the user typed something else
_this.startLoadingSuggestions(searchText);
if (mention.status !== "loading") {
_this.setState({
mention: __assign(__assign({}, _this.state.mention), { status: "loading" })
});
}
break;
case "inactive":
if (suggestionTriggerCharacters.indexOf(event.key) === -1 ||
!/\s|\(|\[|^.{0}$/.test(value.charAt(_this.getTextArea().selectionStart - 1))) {
return;
}
_this.loadEmptySuggestion(event.currentTarget, event.key);
break;
}
};
_this.state = { mention: { status: "inactive", suggestions: [] } };
return _this;
}
TextArea.prototype.suggestionsEnabled = function () {
return (this.props.suggestionTriggerCharacters &&
this.props.suggestionTriggerCharacters.length &&
this.props.loadSuggestions);
};
TextArea.prototype.render = function () {
var _this = this;
var _a = this.props, classes = _a.classes, readOnly = _a.readOnly, textAreaProps = _a.textAreaProps, height = _a.height, heightUnits = _a.heightUnits, value = _a.value, suggestionTriggerCharacters = _a.suggestionTriggerCharacters, loadSuggestions = _a.loadSuggestions, suggestionsDropdownClasses = _a.suggestionsDropdownClasses, textAreaComponent = _a.textAreaComponent, onPaste = _a.onPaste, onDrop = _a.onDrop;
var suggestionsEnabled = suggestionTriggerCharacters &&
suggestionTriggerCharacters.length &&
loadSuggestions;
var mention = this.state.mention;
var TextAreaComponent = (textAreaComponent ||
"textarea");
var heightVal = height && heightUnits ? height + heightUnits : height;
return (React.createElement("div", { className: "mde-textarea-wrapper" },
React.createElement(TextAreaComponent, __assign({ className: ClassNames_1.classNames("mde-text", classes), style: { height: heightVal }, ref: this.props.refObject, readOnly: readOnly, value: value, "data-testid": "text-area" }, textAreaProps, { onChange: function (event) {
var _a;
(_a = textAreaProps === null || textAreaProps === void 0 ? void 0 : textAreaProps.onChange) === null || _a === void 0 ? void 0 : _a.call(textAreaProps, event);
_this.handleOnChange(event);
}, onBlur: function (event) {
var _a;
(_a = textAreaProps === null || textAreaProps === void 0 ? void 0 : textAreaProps.onBlur) === null || _a === void 0 ? void 0 : _a.call(textAreaProps, event);
if (suggestionsEnabled) {
_this.handleBlur();
}
}, onKeyDown: function (event) {
var _a;
(_a = textAreaProps === null || textAreaProps === void 0 ? void 0 : textAreaProps.onKeyDown) === null || _a === void 0 ? void 0 : _a.call(textAreaProps, event);
_this.handleKeyDown(event);
}, onKeyUp: function (event) {
var _a;
(_a = textAreaProps === null || textAreaProps === void 0 ? void 0 : textAreaProps.onKeyUp) === null || _a === void 0 ? void 0 : _a.call(textAreaProps, event);
if (suggestionsEnabled) {
_this.handleKeyUp(event);
}
}, onKeyPress: function (event) {
var _a;
(_a = textAreaProps === null || textAreaProps === void 0 ? void 0 : textAreaProps.onKeyPress) === null || _a === void 0 ? void 0 : _a.call(textAreaProps, event);
if (suggestionsEnabled) {
_this.handleKeyPress(event);
}
}, onPaste: function (event) {
var _a;
(_a = textAreaProps === null || textAreaProps === void 0 ? void 0 : textAreaProps.onPaste) === null || _a === void 0 ? void 0 : _a.call(textAreaProps, event);
onPaste(event);
}, onDragOver: function (event) {
event.preventDefault();
event.stopPropagation();
}, onDrop: function (event) {
var _a;
(_a = textAreaProps === null || textAreaProps === void 0 ? void 0 : textAreaProps.onDrop) === null || _a === void 0 ? void 0 : _a.call(textAreaProps, event);
onDrop(event);
event.preventDefault();
} })),
mention.status === "active" && mention.suggestions.length && (React.createElement(SuggestionsDropdown_1.SuggestionsDropdown, { classes: suggestionsDropdownClasses, caret: mention.caret, suggestions: mention.suggestions, onSuggestionSelected: this.handleSuggestionSelected, suggestionsAutoplace: this.props.suggestionsAutoplace, focusIndex: mention.focusIndex, textAreaRef: this.props.refObject }))));
};
return TextArea;
}(React.Component));
exports.TextArea = TextArea;