fix(composer-emojis): Fix bugs around null nodes and whitespace

Summary: Corrected the linting errors and changed some of the logic around tracking typed text and triggering properly.

Test Plan: Tested locally.

Reviewers: evan, bengotow

Reviewed By: bengotow

Differential Revision: https://phab.nylas.com/D2522
This commit is contained in:
Jackie Luo 2016-02-03 17:40:28 -08:00
parent 3b49e132ab
commit dec0e95dc2
2 changed files with 90 additions and 100 deletions

View file

@ -6,7 +6,7 @@ class EmojiPicker extends React.Component {
static displayName = "EmojiPicker"
static propTypes = {
emojiOptions: React.PropTypes.array,
selectedEmoji: React.PropTypes.string
selectedEmoji: React.PropTypes.string,
};
constructor(props) {
@ -15,36 +15,34 @@ class EmojiPicker extends React.Component {
}
componentDidUpdate() {
selectedButton = React.findDOMNode(this).querySelector(".emoji-option");
const selectedButton = React.findDOMNode(this).querySelector(".emoji-option");
if (selectedButton) {
selectedButton.scrollIntoViewIfNeeded();
}
}
onMouseDown(emojiChar) {
EmojiActions.selectEmoji({emojiChar});
}
render() {
let emojis = [];
emojiIndex = this.props.emojiOptions.indexOf(this.props.selectedEmoji);
if (emojiIndex == -1) {
emojiIndex = 0;
}
const emojis = [];
let emojiIndex = this.props.emojiOptions.indexOf(this.props.selectedEmoji);
if (emojiIndex === -1) emojiIndex = 0;
if (this.props.emojiOptions) {
this.props.emojiOptions.forEach((emojiOption, i) => {
this.props.emojiOptions.forEach((emojiOption, i) => {
const emojiChar = emoji.get(emojiOption);
emojiClass = emojiIndex == i ? "btn btn-icon emoji-option" : "btn btn-icon";
const emojiClass = emojiIndex === i ? "btn btn-icon emoji-option" : "btn btn-icon";
emojis.push(<button onMouseDown={() => this.onMouseDown(emojiChar)} className={emojiClass}>{emojiChar} :{emojiOption}:</button>);
emojis.push(<br />);
})
}
return(
return (
<div className="emoji-picker">
{emojis}
</div>
);
}
onMouseDown(emojiChar) {
EmojiActions.selectEmoji({emojiChar});
}
}
export default EmojiPicker;

View file

@ -6,25 +6,23 @@ const emojis = Object.keys(emoji.emoji).sort();
class EmojisComposerExtension extends ContenteditableExtension {
static onContentChanged = ({editor, mutations}) => {
sel = editor.currentSelection()
let {emojiOptions, triggerWord} = EmojisComposerExtension._findEmojiOptions(sel);
static onContentChanged = ({editor}) => {
const sel = editor.currentSelection()
const {emojiOptions, triggerWord} = EmojisComposerExtension._findEmojiOptions(sel);
if (sel.anchorNode && sel.isCollapsed) {
let {emojiOptions, triggerWord} = EmojisComposerExtension._findEmojiOptions(sel);
if (emojiOptions.length > 0) {
offset = sel.anchorOffset;
const offset = sel.anchorOffset;
if (!DOMUtils.closest(sel.anchorNode, "n1-emoji-autocomplete")) {
editor.select(sel.anchorNode,
sel.anchorOffset - triggerWord.length,
sel.focusNode,
sel.focusOffset).wrapSelection("n1-emoji-autocomplete");
editor.select(sel.anchorNode,
offset,
sel.anchorNode,
offset);
editor.select(sel.anchorNode,
sel.anchorOffset - triggerWord.length,
sel.focusNode,
sel.focusOffset).wrapSelection("n1-emoji-autocomplete");
editor.select(sel.anchorNode,
offset,
sel.anchorNode,
offset);
}
}
else {
} else {
if (DOMUtils.closest(sel.anchorNode, "n1-emoji-autocomplete")) {
editor.unwrapNodeAndSelectAll(DOMUtils.closest(sel.anchorNode, "n1-emoji-autocomplete"));
editor.select(sel.anchorNode,
@ -33,8 +31,7 @@ class EmojisComposerExtension extends ContenteditableExtension {
sel.focusOffset + triggerWord.length);
}
}
}
else {
} else {
if (DOMUtils.closest(sel.anchorNode, "n1-emoji-autocomplete")) {
editor.unwrapNodeAndSelectAll(DOMUtils.closest(sel.anchorNode, "n1-emoji-autocomplete"));
editor.select(sel.anchorNode,
@ -46,20 +43,21 @@ class EmojisComposerExtension extends ContenteditableExtension {
}
static toolbarComponentConfig = ({toolbarState}) => {
sel = toolbarState.selectionSnapshot;
const sel = toolbarState.selectionSnapshot;
if (sel) {
let {emojiOptions, triggerWord} = EmojisComposerExtension._findEmojiOptions(sel);
const {emojiOptions} = EmojisComposerExtension._findEmojiOptions(sel);
if (emojiOptions.length > 0 && !toolbarState.dragging && !toolbarState.doubleDown) {
locationRefNode = DOMUtils.closest(toolbarState.selectionSnapshot.anchorNode,
"n1-emoji-autocomplete");
emojiNameNode = DOMUtils.closest(sel.anchorNode, "n1-emoji-autocomplete");
selectedEmoji = emojiNameNode.getAttribute("selectedEmoji");
const locationRefNode = DOMUtils.closest(sel.anchorNode,
"n1-emoji-autocomplete");
const emojiNameNode = DOMUtils.closest(sel.anchorNode,
"n1-emoji-autocomplete");
const selectedEmoji = emojiNameNode.getAttribute("selectedEmoji");
return {
component: EmojiPicker,
props: {emojiOptions,
selectedEmoji},
locationRefNode: locationRefNode,
width: EmojisComposerExtension._emojiPickerWidth(emojiOptions)
width: EmojisComposerExtension._emojiPickerWidth(emojiOptions),
}
}
}
@ -69,42 +67,38 @@ class EmojisComposerExtension extends ContenteditableExtension {
static editingActions = () => {
return [{
action: EmojiActions.selectEmoji,
callback: EmojisComposerExtension._onSelectEmoji
callback: EmojisComposerExtension._onSelectEmoji,
}]
}
static onKeyDown = ({editor, event}) => {
sel = editor.currentSelection()
let {emojiOptions, triggerWord} = EmojisComposerExtension._findEmojiOptions(sel);
const sel = editor.currentSelection()
const {emojiOptions} = EmojisComposerExtension._findEmojiOptions(sel);
if (emojiOptions.length > 0) {
if (event.key == "ArrowDown" || event.key == "ArrowRight" ||
event.key == "ArrowUp" || event.key == "ArrowLeft") {
if (event.key === "ArrowDown" || event.key === "ArrowRight" ||
event.key === "ArrowUp" || event.key === "ArrowLeft") {
event.preventDefault();
moveToNext = (event.key == "ArrowDown" || event.key == "ArrowRight")
emojiNameNode = DOMUtils.closest(sel.anchorNode, "n1-emoji-autocomplete");
selectedEmoji = emojiNameNode.getAttribute("selectedEmoji");
const moveToNext = (event.key === "ArrowDown" || event.key === "ArrowRight")
const emojiNameNode = DOMUtils.closest(sel.anchorNode, "n1-emoji-autocomplete");
const selectedEmoji = emojiNameNode.getAttribute("selectedEmoji");
if (selectedEmoji) {
emojiIndex = emojiOptions.indexOf(selectedEmoji);
const emojiIndex = emojiOptions.indexOf(selectedEmoji);
if (emojiIndex < emojiOptions.length - 1 && moveToNext) {
emojiNameNode.setAttribute("selectedEmoji", emojiOptions[emojiIndex + 1]);
}
else if (emojiIndex > 0 && !moveToNext) {
} else if (emojiIndex > 0 && !moveToNext) {
emojiNameNode.setAttribute("selectedEmoji", emojiOptions[emojiIndex - 1]);
}
else {
index = moveToNext ? 0 : emojiOptions.length - 1;
} else {
const index = moveToNext ? 0 : emojiOptions.length - 1;
emojiNameNode.setAttribute("selectedEmoji", emojiOptions[index]);
}
}
else {
index = moveToNext ? 1 : emojiOptions.length - 1;
} else {
const index = moveToNext ? 1 : emojiOptions.length - 1;
emojiNameNode.setAttribute("selectedEmoji", emojiOptions[index]);
}
}
else if (event.key == "Enter") {
} else if (event.key === "Enter") {
event.preventDefault();
emojiNameNode = DOMUtils.closest(sel.anchorNode, "n1-emoji-autocomplete");
selectedEmoji = emojiNameNode.getAttribute("selectedEmoji");
const emojiNameNode = DOMUtils.closest(sel.anchorNode, "n1-emoji-autocomplete");
let selectedEmoji = emojiNameNode.getAttribute("selectedEmoji");
if (!selectedEmoji) selectedEmoji = emojiOptions[0];
EmojisComposerExtension._onSelectEmoji({editor: editor,
actionArg: {emojiChar: emoji.get(selectedEmoji)}});
@ -117,19 +111,19 @@ class EmojisComposerExtension extends ContenteditableExtension {
sel.anchorNode.nodeValue &&
sel.anchorNode.nodeValue.length > 0 &&
sel.isCollapsed) {
words = sel.anchorNode.nodeValue.substring(0, sel.anchorOffset).split(" ");
lastWord = words[words.length - 1];
if (words.length == 1 &&
lastWord.indexOf(" ") == -1 &&
lastWord.indexOf(":") == -1) {
let {text, textNode} = EmojisComposerExtension._getTextUntilSpace(sel.anchorNode, sel.anchorOffset);
const words = sel.anchorNode.nodeValue.substring(0, sel.anchorOffset).split(" ");
let lastWord = words[words.length - 1].trim();
if (words.length === 1 &&
lastWord.indexOf(" ") === -1 &&
lastWord.indexOf(":") === -1) {
const {text} = EmojisComposerExtension._getTextUntilSpace(sel.anchorNode, sel.anchorOffset);
lastWord = text;
}
if (lastWord.length > 1 &&
lastWord.charAt(0) == ":" &
lastWord.charAt(lastWord.length - 1) != " ") {
let word = lastWord.substring(1).trim();
if (lastWord.charAt(lastWord.length - 1) == ":") {
lastWord.charAt(0) === ":" &
lastWord.charAt(lastWord.length - 1) !== " ") {
let word = lastWord.substring(1);
if (lastWord.charAt(lastWord.length - 1) === ":") {
word = word.substring(0, word.length - 1);
}
return {triggerWord: lastWord, emojiOptions: EmojisComposerExtension._findMatches(word)};
@ -140,27 +134,26 @@ class EmojisComposerExtension extends ContenteditableExtension {
}
static _onSelectEmoji = ({editor, actionArg}) => {
emojiChar = actionArg.emojiChar;
const emojiChar = actionArg.emojiChar;
if (!emojiChar) return null;
sel = editor.currentSelection()
const sel = editor.currentSelection()
if (sel.anchorNode &&
sel.anchorNode.nodeValue &&
sel.anchorNode.nodeValue.length > 0 &&
sel.isCollapsed) {
words = sel.anchorNode.nodeValue.substring(0, sel.anchorOffset).split(" ");
lastWord = words[words.length - 1];
if (words.length == 1 &&
lastWord.indexOf(" ") == -1 &&
lastWord.indexOf(":") == -1) {
let {text, textNode} = EmojisComposerExtension._getTextUntilSpace(sel.anchorNode, sel.anchorOffset);
lastWord = text;
offset = textNode.nodeValue.lastIndexOf(":");
editor.select(textNode,
offset,
sel.focusNode,
sel.focusOffset);
}
else {
const words = sel.anchorNode.nodeValue.substring(0, sel.anchorOffset).split(" ");
let lastWord = words[words.length - 1].trim();
if (words.length === 1 &&
lastWord.indexOf(" ") === -1 &&
lastWord.indexOf(":") === -1) {
const {text, textNode} = EmojisComposerExtension._getTextUntilSpace(sel.anchorNode, sel.anchorOffset);
lastWord = text;
const offset = textNode.nodeValue.lastIndexOf(":");
editor.select(textNode,
offset,
sel.focusNode,
sel.focusOffset);
} else {
editor.select(sel.anchorNode,
sel.anchorOffset - lastWord.length,
sel.focusNode,
@ -171,38 +164,37 @@ class EmojisComposerExtension extends ContenteditableExtension {
}
static _emojiPickerWidth(emojiOptions) {
let max_length = 0;
for (emojiOption of emojiOptions) {
if (emojiOption.length > max_length) {
max_length = emojiOption.length;
let maxLength = 0;
for (const emojiOption of emojiOptions) {
if (emojiOption.length > maxLength) {
maxLength = emojiOption.length;
}
}
WIDTH_PER_CHAR = 8;
return (max_length + 10) * WIDTH_PER_CHAR;
const WIDTH_PER_CHAR = 8;
return (maxLength + 10) * WIDTH_PER_CHAR;
}
static _getTextUntilSpace(node, offset) {
text = node.nodeValue.substring(0, offset);
prevTextNode = DOMUtils.previousTextNode(node);
let text = node.nodeValue.substring(0, offset);
let prevTextNode = DOMUtils.previousTextNode(node);
if (!prevTextNode) return {text: text, textNode: node};
while (prevTextNode) {
if (prevTextNode.nodeValue.indexOf(" ") == -1 &&
prevTextNode.nodeValue.indexOf(":") == -1) {
if (prevTextNode.nodeValue.indexOf(" ") === -1 &&
prevTextNode.nodeValue.indexOf(":") === -1) {
text = prevTextNode.nodeValue + text;
prevTextNode = DOMUtils.previousTextNode(prevTextNode);
}
else {
} else {
text = prevTextNode.nodeValue.trim() + text;
break;
}
}
text = prevTextNode.nodeValue.trim() + text;
return {text: text, textNode: prevTextNode};
}
static _findMatches(word) {
emojiOptions = []
const emojiOptions = []
for (const curEmoji of emojis) {
if (word == curEmoji.substring(0, word.length)) {
if (word === curEmoji.substring(0, word.length)) {
emojiOptions.push(curEmoji);
}
}