Skip to content

Commit

Permalink
Implement synonyms search and inserting
Browse files Browse the repository at this point in the history
  • Loading branch information
y-moroz committed May 10, 2020
1 parent 83f9295 commit c6fabb7
Show file tree
Hide file tree
Showing 10 changed files with 223 additions and 3 deletions.
44 changes: 44 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"awesome-debounce-promise": "^2.1.0",
"classnames": "^2.2.6",
"prop-types": "^15.7.2",
"ramda": "^0.27.0",
"react": "^16.2.0",
"react-async-hook": "^3.6.1",
"react-dom": "^16.2.0",
"react-icons": "^3.10.0",
"react-scripts": "1.1.1"
"react-scripts": "1.1.1",
"use-constant": "^1.0.0"
},
"scripts": {
"start": "react-scripts start",
Expand Down
2 changes: 1 addition & 1 deletion src/text-editor/components/file-zone/FileZone.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import './FileZone.css';
function FileZone(props) {
return (
<div id="file-zone">
<div id="file" contentEditable={true}>
<div id="file" contentEditable={true} suppressContentEditableWarning>
{props.children}
</div>
</div>
Expand Down
4 changes: 3 additions & 1 deletion src/text-editor/plugins/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { SIMPLE_ACTION_PLUGIN_CONFIG } from './simple-action';
import { SYNONYMS_ACTION_PLUGIN_CONFIG } from './synonyms';

const PLUGINS = [
...SIMPLE_ACTION_PLUGIN_CONFIG
...SIMPLE_ACTION_PLUGIN_CONFIG,
...SYNONYMS_ACTION_PLUGIN_CONFIG
];

export default PLUGINS;
37 changes: 37 additions & 0 deletions src/text-editor/plugins/synonyms/Synonyms.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.synonyms {
position: absolute;
background: #ffffff;
border: 1px solid #bebebe;
box-shadow: 1px 1px 1px #bebebe;
}

.synonyms__title {
background-color: #e9e9e9;
border-bottom: 1px solid #ccc;
font-size: 14px;
padding: 4px;
}

.synonyms-list {
list-style: none;
padding: 0;
margin: 0;
min-width: 150px;
max-height: 250px;
overflow-y: auto;
}

.synonyms-list__button {
background: none;
border: none;
width: 100%;
padding: 5px 15px 5px 15px;
font-size: 14px;
cursor: pointer;
text-align: left;
border-bottom: 1px solid #eee;
}

.synonyms-list__button:hover {
background: #f1f0f0;
}
63 changes: 63 additions & 0 deletions src/text-editor/plugins/synonyms/Synonyms.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React, { useEffect, useLayoutEffect, useRef } from 'react';
import useConstant from 'use-constant';
import { useSearchSynonyms } from './hooks';
import './Synonyms.css';
import { replaceSelectedText } from '../../shared/selection-helpers';

function Synonyms() {
const {synonyms, setText} = useSearchSynonyms();
const listRefContainer = useRef(null);

const selectionChangeListener = useConstant(() => () => {
const selection = document.getSelection();
if (selection) {
setText(selection.toString());
}
});

useEffect(
() => {
document.addEventListener('selectionchange', selectionChangeListener);
return () => document.removeEventListener('selectionchange', selectionChangeListener);
},
[]
);

useLayoutEffect(
() => {
const selection = document.getSelection();
if (selection && selection.rangeCount > 0 && listRefContainer.current) {
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
listRefContainer.current.style.top = `${rect.top + rect.height + 2}px`;
listRefContainer.current.style.left = `${rect.left - 4}px`;
}
},
[synonyms.result]
);

if (synonyms.result && synonyms.result.length) {
return (
<div className="synonyms" ref={listRefContainer}>
<div className="synonyms__title">Select word to replace:</div>

<ul className="synonyms-list">
{synonyms.result.map(({word}) => (
<li key={word}>
<button
type="button"
className="synonyms-list__button"
onClick={() => replaceSelectedText(word)}
>{word}</button>
</li>
))}
</ul>
</div>
);
}

return null;
}

export default Synonyms;

35 changes: 35 additions & 0 deletions src/text-editor/plugins/synonyms/hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useState } from 'react';
import useConstant from 'use-constant';
import AwesomeDebouncePromise from 'awesome-debounce-promise';
import SynonymsApi from './synonyms-api';
import { useAsync } from 'react-async-hook';

const useSearchSynonyms = () => {
const [text, setText] = useState('');

const debouncedLoadSynonyms = useConstant(() => AwesomeDebouncePromise(SynonymsApi.loadSynonyms, 300));

const synonyms = useAsync(
async text => {
if (text.trim().length === 0) {
return [];
}
return debouncedLoadSynonyms(text.trim())
.catch(error => {
console.error(`Error occurred while fetching synonyms: ${error.message}`);
return [];
});
},
[text]
);

return {
text,
synonyms,
setText
};
};

export {
useSearchSynonyms
};
5 changes: 5 additions & 0 deletions src/text-editor/plugins/synonyms/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Synonyms from './Synonyms';

export const SYNONYMS_ACTION_PLUGIN_CONFIG = [{
component: Synonyms
}];
13 changes: 13 additions & 0 deletions src/text-editor/plugins/synonyms/synonyms-api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const SYNONYMS_API_ROOT = 'https://api.datamuse.com/words';

const loadSynonyms = async word => {
const response = await fetch(`${SYNONYMS_API_ROOT}?rel_syn=${word}`);
if (!response.ok || response.status !== 200) {
throw new Error('Failed to load synonyms');
}
return response.json();
};

export default {
loadSynonyms
};
18 changes: 18 additions & 0 deletions src/text-editor/shared/selection-helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const replaceSelectedText = text => {
const selection = document.getSelection();
if (selection.rangeCount) {
// keep accidentally selected space after the word
if (/.+?\s+$/.test(selection.toString())) {
selection.extend(selection.focusNode, selection.focusOffset - 1);
}

const range = selection.getRangeAt(0);
range.deleteContents();
range.insertNode(document.createTextNode(text));
selection.collapseToEnd();
}
}

export {
replaceSelectedText
}

0 comments on commit c6fabb7

Please sign in to comment.