From 679669a7fa14eefd04012d508793a76d64feb216 Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Sun, 16 Jun 2024 10:43:14 +0000 Subject: [PATCH 01/26] adding spell checker as an enabled mode --- config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.ts b/config.ts index 55939b288..cb8896f01 100644 --- a/config.ts +++ b/config.ts @@ -6,7 +6,7 @@ export default { apyURL: 'https://beta.apertium.org/apy', defaultMode: Mode.Translation, - enabledModes: new Set([Mode.Translation, Mode.Analysis, Mode.Generation, Mode.Sandbox]), + enabledModes: new Set([Mode.Translation, Mode.Analysis, Mode.Generation, Mode.Sandbox, Mode.SpellChecker]), translationChaining: true, subtitle: 'Beta', From 896a77e7f091a65c918e882b2d357c5b073134eb Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Sun, 16 Jun 2024 10:45:01 +0000 Subject: [PATCH 02/26] specifying path to spell checker --- src/App.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/App.tsx b/src/App.tsx index 8780ccb22..1a3cc5196 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,6 +14,7 @@ import { langDirection, toAlpha2Code } from './util/languages'; import { Mode } from './types'; import Analyzer from './components/Analyzer'; +import SpellChecker from './components/SpellChecker'; import { Path as DocTranslationPath } from './components/translator/DocTranslationForm'; import Footer from './components/footer'; import Generator from './components/Generator'; @@ -31,6 +32,7 @@ const Interfaces = { [Mode.Analysis]: Analyzer, [Mode.Generation]: Generator, [Mode.Sandbox]: Sandbox, + [Mode.SpellChecker]: SpellChecker, } as Record>; const App = ({ setLocale }: { setLocale: React.Dispatch> }): React.ReactElement => { @@ -127,6 +129,12 @@ const App = ({ setLocale }: { setLocale: React.Dispatch )} + {enabledModes.has(Mode.SpellChecker) && ( + + + // write your spellchecker here + + )}
From a6958e2998bcd67fe76af1e1df2b24df8ad03096 Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Sun, 16 Jun 2024 10:46:59 +0000 Subject: [PATCH 03/26] included spell checker as a new mode --- src/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types.ts b/src/types.ts index b6caa1c65..71576cbb6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,6 +5,7 @@ export enum Mode { Analysis = 'analysis', Generation = 'generation', Sandbox = 'sandbox', + SpellChecker = 'spellchecker' } export type Config = { From 12eb79341fb31f1d1a8645921874b404548a8f3c Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Sun, 16 Jun 2024 10:50:08 +0000 Subject: [PATCH 04/26] adding spell check as a link in the nav bar --- src/components/navbar/index.tsx | 12 ++++++++++++ src/strings/eng.json | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/components/navbar/index.tsx b/src/components/navbar/index.tsx index 51347cb76..7b4959ce1 100644 --- a/src/components/navbar/index.tsx +++ b/src/components/navbar/index.tsx @@ -107,6 +107,18 @@ const NavbarNav: React.ComponentType = (props: NavProps) => { )} + {enabledModes.has(Mode.SpellChecker) && ( + + + pathname === '/spellchecker' || (pathname === '/' && defaultMode === Mode.SpellChecker) + } + to={'/spellchecker'} + > + {t('Spell_Check')} + + + )} ); }; diff --git a/src/strings/eng.json b/src/strings/eng.json index cc7d6a706..fbf728ab7 100644 --- a/src/strings/eng.json +++ b/src/strings/eng.json @@ -64,5 +64,6 @@ "Downloads_Para": "Current versions of the Apertium toolbox as well as of language-pair data are available from the GitHub page. Installation instructions for Apertium on all major platforms are provided on the Wiki's Installation page.", "Contact_Para": "

IRC channel

The quickest way to contact us is by joining our IRC channel, #apertium at irc.oftc.net, where users and developers of Apertium meet. You don't need an IRC client; you can use OFTC webchat.

Mailing list

Also, subscribe to the apertium-stuff mailing list, where you can post longer proposals or issues, as well as follow general Apertium discussions.

Contact

Feel free to contact us via the apertium-contact mailing list if you find a mistake, there's a project you would like to see us work on, or you would like to help out.

", "Install_Apertium": "Install Apertium", - "Install_Apertium_Para": "Are you experiencing slow responses? Our servers might be overloaded. Learn how to install Apertium locally." + "Install_Apertium_Para": "Are you experiencing slow responses? Our servers might be overloaded. Learn how to install Apertium locally.", + "Spell_Check" : "Spell Checker" } From 013830960c06131416b56a728e332b2eeeaa95a6 Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Mon, 17 Jun 2024 04:09:27 +0000 Subject: [PATCH 05/26] empty initial spell check component --- src/components/spellchecker/SpellChecker.tsx | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/components/spellchecker/SpellChecker.tsx diff --git a/src/components/spellchecker/SpellChecker.tsx b/src/components/spellchecker/SpellChecker.tsx new file mode 100644 index 000000000..7c8c55420 --- /dev/null +++ b/src/components/spellchecker/SpellChecker.tsx @@ -0,0 +1,7 @@ +import * as React from 'react'; + +const SpellChecker = () : React.ReactElement => { + return <> +} + +export default SpellChecker \ No newline at end of file From 03ee1b5a1c976b465e3bd2b7cda548e2f59d9496 Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Mon, 17 Jun 2024 04:09:54 +0000 Subject: [PATCH 06/26] fixing import for spell check component --- src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index 1a3cc5196..af06f15e7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,7 +14,7 @@ import { langDirection, toAlpha2Code } from './util/languages'; import { Mode } from './types'; import Analyzer from './components/Analyzer'; -import SpellChecker from './components/SpellChecker'; +import SpellChecker from './components/spellchecker/SpellChecker'; import { Path as DocTranslationPath } from './components/translator/DocTranslationForm'; import Footer from './components/footer'; import Generator from './components/Generator'; From 530cd5be7e621ef25ec5471c15d612e47caf69f2 Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Mon, 17 Jun 2024 09:08:08 +0000 Subject: [PATCH 07/26] initial code for spell checker interface --- build.ts | 10 +- src/components/spellchecker/SpellChecker.tsx | 177 ++++++++++++++++++- src/components/spellchecker/spellchecker.css | 32 ++++ src/testSetup.ts | 3 + 4 files changed, 217 insertions(+), 5 deletions(-) create mode 100644 src/components/spellchecker/spellchecker.css diff --git a/build.ts b/build.ts index 9dd903416..45d244594 100755 --- a/build.ts +++ b/build.ts @@ -66,12 +66,13 @@ const Plugin = { let defaultStrings: unknown; - let pairsResponse, analyzersResponse, generatorsResponse; + let pairsResponse, analyzersResponse, generatorsResponse, spellerResponse; try { - [pairsResponse, analyzersResponse, generatorsResponse] = await Promise.all([ + [pairsResponse, analyzersResponse, generatorsResponse, spellerResponse] = await Promise.all([ apyGet('list', {}), apyGet('list', { q: 'analyzers' }), apyGet('list', { q: 'generators' }), + apyGet('list', { q: 'spellers' }), ]); } catch (error) { let message = new String(error).toString(); @@ -104,6 +105,9 @@ const Plugin = { const generators = Object.fromEntries( Object.entries(generatorsResponse.data as Record).filter(([code]) => allowedLang(code)), ); + const spellers = Object.fromEntries( + Object.entries(spellerResponse.data as Record).filter(([code]) => allowedLang(code)), + ); let pairPrefs = {}; try { @@ -116,6 +120,7 @@ const Plugin = { ...pairs.flatMap(({ sourceLanguage, targetLanguage }) => [sourceLanguage, targetLanguage]), ...Object.keys(analyzers), ...Object.keys(generators), + ...Object.keys(spellers), ...Object.keys(languages), ...Object.keys(locales), ].filter(Boolean); @@ -170,6 +175,7 @@ const Plugin = { 'window.PAIR_PREFS': JSON.stringify(pairPrefs), 'window.ANALYZERS': JSON.stringify(analyzers), 'window.GENERATORS': JSON.stringify(generators), + 'window.SPELLERS': JSON.stringify(spellers), ...initialOptions.define, }; diff --git a/src/components/spellchecker/SpellChecker.tsx b/src/components/spellchecker/SpellChecker.tsx index 7c8c55420..85a3ff1dd 100644 --- a/src/components/spellchecker/SpellChecker.tsx +++ b/src/components/spellchecker/SpellChecker.tsx @@ -1,7 +1,178 @@ import * as React from 'react'; +import Button from 'react-bootstrap/Button'; +import Col from 'react-bootstrap/Col'; +import Form from 'react-bootstrap/Form'; +import { useHistory } from 'react-router-dom'; +import { useMatomo } from '@datapunt/matomo-tracker-react'; +import { MaxURLLength, buildNewSearch, getUrlParam } from '../../util/url'; +import { toAlpha3Code } from '../../util/languages'; +import useLocalStorage from '../../util/useLocalStorage'; +import { useLocalization } from '../../util/localization'; +import './spellchecker.css'; -const SpellChecker = () : React.ReactElement => { - return <> +interface Suggestion { + token: string; + known: boolean; + sugg: Array<[string, number]>; } -export default SpellChecker \ No newline at end of file +const Spellers: Readonly> = (window as any).SPELLERS; + +const langUrlParam = 'lang'; +const textUrlParam = 'q'; + +const SpellChecker = (): React.ReactElement => { + const history = useHistory(); + const { t, tLang } = useLocalization(); + const { trackEvent } = useMatomo(); + // const apyFetch = React.useContext(APyContext); // Commenting out for now + const [suggestions, setSuggestions] = React.useState([]); + const [selectedWord, setSelectedWord] = React.useState(null); + const [loading, setLoading] = React.useState(false); + const [error, setError] = React.useState(null); + + const [lang, setLang] = useLocalStorage('spellerLang', Object.keys(Spellers)[0], { + overrideValue: toAlpha3Code(getUrlParam(history.location.search, langUrlParam)), + validateValue: (l) => l in Spellers, + }); + + const [text, setText] = useLocalStorage('spellerText', '', { + overrideValue: getUrlParam(history.location.search, textUrlParam), + }); + + React.useEffect(() => { + let search = buildNewSearch({ [langUrlParam]: lang, [textUrlParam]: text }); + if (search.length > MaxURLLength) { + search = buildNewSearch({ [langUrlParam]: lang }); + } + history.replace({ search }); + }, [history, lang, text]); + + const spellcheckRef = React.useRef(null); + + const handleSubmit = () => { + if (text.trim().length === 0) { + return; + } + + // Simulating the API call + setLoading(true); + setTimeout(() => { + setSuggestions(checkSpelling(text)); + setError(null); + setLoading(false); + }, 1000); + }; + + const handleInput = (e: React.FormEvent) => { + setText(e.currentTarget.innerText); + }; + + const handleWordClick = (word: string) => { + setSelectedWord(word); + }; + + const applySuggestion = (suggestion: string) => { + if (!selectedWord) return; + const updatedText = text.replace(new RegExp(`\\b${selectedWord}\\b`, 'g'), suggestion); + setText(updatedText); + setSelectedWord(null); + + // Remove the applied suggestion from the suggestions list + setSuggestions((prevSuggestions) => + prevSuggestions.filter((s) => s.token !== selectedWord) + ); + }; + + const checkSpelling = (inputText: string): Suggestion[] => { + // Simulated response from the spell checker + return [ + { + token: 'Thiss', + known: false, + sugg: [ + ['This', 0.9], + ['Thus', 0.1], + ], + }, + { + token: 'exampel', + known: false, + sugg: [ + ['example', 0.95], + ['exemplar', 0.05], + ], + }, + ]; + }; + + const renderHighlightedText = () => { + const parts = text.split(/(\s+)/).map((word, index) => { + const suggestion = suggestions.find((s) => s.token === word && !s.known); + if (suggestion) { + return ( + handleWordClick(word)}> + {word} + + ); + } + return word; + }); + return <>{parts}; + }; + + return ( +
event.preventDefault()}> + + {t('Language')} + + setLang(value)} required value={lang}> + {Object.keys(Spellers) + .map((code) => [code, tLang(code)]) + .sort(([, a], [, b]) => { + return a.toLowerCase().localeCompare(b.toLowerCase()); + }) + .map(([code, name]) => ( + + ))} + + + + + {t('Input_Text')} + +
+ {renderHighlightedText()} +
+ +
+ + + + + + {selectedWord && ( +
+ {suggestions + .find((s) => s.token === selectedWord) + ?.sugg.map(([sugg, _], index) => ( +
applySuggestion(sugg)}> + {sugg} +
+ ))} +
+ )} +
+ ); +}; + +export default SpellChecker; diff --git a/src/components/spellchecker/spellchecker.css b/src/components/spellchecker/spellchecker.css new file mode 100644 index 000000000..c57c4d077 --- /dev/null +++ b/src/components/spellchecker/spellchecker.css @@ -0,0 +1,32 @@ +.content-editable { + border: 1px solid #ccc; + padding: 10px; + min-height: 150px; + white-space: pre-wrap; + word-wrap: break-word; + } + + .misspelled { + color: red; + text-decoration: underline wavy red; + cursor: pointer; + } + + .suggestions { + position: absolute; + background: white; + border: 1px solid #ccc; + z-index: 1000; + max-height: 150px; + overflow-y: auto; + } + + .suggestions div { + padding: 5px; + cursor: pointer; + } + + .suggestions div:hover { + background-color: #f0f0f0; + } + \ No newline at end of file diff --git a/src/testSetup.ts b/src/testSetup.ts index 19c637dcd..a1ac5ede3 100644 --- a/src/testSetup.ts +++ b/src/testSetup.ts @@ -32,6 +32,9 @@ defaultStrings['Maintainer'] = '{{maintainer}}-Default'; // eslint-disable-next-line (window as any).GENERATORS = { eng: 'eng-gener', spa: 'spa-gener' }; +// eslint-disable-next-line +(window as any).SPELLERS = { hin: "hin-spell", kaz: "kaz-spell"}; + process.on('unhandledRejection', (err) => { // eslint-disable-next-line jest/no-jasmine-globals fail(err); From 46ca008b3cafe13e89c5a0d097158bb215fa8380 Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Fri, 21 Jun 2024 11:13:45 +0000 Subject: [PATCH 08/26] fix issues with text rendering --- package.json | 1 + src/App.tsx | 7 +- src/components/spellchecker/SpellChecker.tsx | 112 +++++++++---------- src/components/spellchecker/spellchecker.css | 27 ++++- src/testSetup.ts | 2 +- src/types.ts | 2 +- 6 files changed, 80 insertions(+), 71 deletions(-) diff --git a/package.json b/package.json index 34b7ea24d..ee6c7a5ce 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "author": "Sushain Cherivirala ", "license": "GPL-3.0-or-later", "scripts": { + "start": "yarn build && yarn serve", "build": "ts-node build.ts", "serve": "python3 -m http.server --directory dist", "tsc": "tsc --noEmit", diff --git a/src/App.tsx b/src/App.tsx index af06f15e7..79d788755 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -130,10 +130,9 @@ const App = ({ setLocale }: { setLocale: React.Dispatch )} {enabledModes.has(Mode.SpellChecker) && ( - - - // write your spellchecker here - + + // write your spellchecker here + )}
diff --git a/src/components/spellchecker/SpellChecker.tsx b/src/components/spellchecker/SpellChecker.tsx index 85a3ff1dd..6f3a5bcdc 100644 --- a/src/components/spellchecker/SpellChecker.tsx +++ b/src/components/spellchecker/SpellChecker.tsx @@ -25,8 +25,24 @@ const SpellChecker = (): React.ReactElement => { const history = useHistory(); const { t, tLang } = useLocalization(); const { trackEvent } = useMatomo(); - // const apyFetch = React.useContext(APyContext); // Commenting out for now - const [suggestions, setSuggestions] = React.useState([]); + const [suggestions, setSuggestions] = React.useState([ + { + token: 'Thiss', + known: false, + sugg: [ + ['This', 0.9], + ['Thus', 0.1], + ], + }, + { + token: 'exampel', + known: false, + sugg: [ + ['example', 0.95], + ['exemplar', 0.05], + ], + }, + ]); const [selectedWord, setSelectedWord] = React.useState(null); const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState(null); @@ -50,75 +66,53 @@ const SpellChecker = (): React.ReactElement => { const spellcheckRef = React.useRef(null); - const handleSubmit = () => { - if (text.trim().length === 0) { - return; - } - - // Simulating the API call - setLoading(true); - setTimeout(() => { - setSuggestions(checkSpelling(text)); - setError(null); - setLoading(false); - }, 1000); - }; - const handleInput = (e: React.FormEvent) => { setText(e.currentTarget.innerText); }; const handleWordClick = (word: string) => { + console.log("Word clicked:", word); setSelectedWord(word); }; const applySuggestion = (suggestion: string) => { if (!selectedWord) return; - const updatedText = text.replace(new RegExp(`\\b${selectedWord}\\b`, 'g'), suggestion); - setText(updatedText); - setSelectedWord(null); - // Remove the applied suggestion from the suggestions list - setSuggestions((prevSuggestions) => - prevSuggestions.filter((s) => s.token !== selectedWord) - ); - }; + const updatedText = text.replace(new RegExp(`\\b${selectedWord}\\b`, 'g'), suggestion); + setText(updatedText); // Schedule state update - const checkSpelling = (inputText: string): Suggestion[] => { - // Simulated response from the spell checker - return [ - { - token: 'Thiss', - known: false, - sugg: [ - ['This', 0.9], - ['Thus', 0.1], - ], - }, - { - token: 'exampel', - known: false, - sugg: [ - ['example', 0.95], - ['exemplar', 0.05], - ], - }, - ]; + console.log(text) + setSelectedWord(null); + renderHighlightedText() }; const renderHighlightedText = () => { - const parts = text.split(/(\s+)/).map((word, index) => { - const suggestion = suggestions.find((s) => s.token === word && !s.known); - if (suggestion) { - return ( - handleWordClick(word)}> - {word} - - ); - } - return word; - }); - return <>{parts}; + if (text.trim().length === 0) { + return; + } + console.log(text) + console.log("yo i got called!!!") + + const contentElement = spellcheckRef.current; + if (contentElement instanceof HTMLElement) { + + const parts = text.split(/(\s+)/).map((word, index) => { + const suggestion = suggestions.find((s) => s.token === word && !s.known); + if (suggestion) { + return `${word}`; + } else { + return `${word}`; + } + }).join(''); + + contentElement.innerHTML = parts; + + const misspelledElements = contentElement.querySelectorAll('.misspelled'); + misspelledElements.forEach((element, index) => { + const word = element.textContent || ''; + element.addEventListener('click', () => handleWordClick(word)); + }); + } }; return ( @@ -148,14 +142,12 @@ const SpellChecker = (): React.ReactElement => { contentEditable ref={spellcheckRef} onInput={handleInput} - > - {renderHighlightedText()} -
+ /> - diff --git a/src/components/spellchecker/spellchecker.css b/src/components/spellchecker/spellchecker.css index c57c4d077..a3c7fda74 100644 --- a/src/components/spellchecker/spellchecker.css +++ b/src/components/spellchecker/spellchecker.css @@ -7,20 +7,37 @@ } .misspelled { - color: red; + position: relative; + display: inline-block; text-decoration: underline wavy red; cursor: pointer; } - .suggestions { - position: absolute; + .suggestions { background: white; border: 1px solid #ccc; - z-index: 1000; max-height: 150px; overflow-y: auto; + text-align: center; + border-radius: 6px; + padding: 5px 0; + position: absolute; + z-index: 1; + top: 150%; + left: 50%; + margin-left: -60px; } - + .suggestions::after { + content: ""; + position: absolute; + bottom: 100%; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: transparent transparent black transparent; + } + .suggestions div { padding: 5px; cursor: pointer; diff --git a/src/testSetup.ts b/src/testSetup.ts index a1ac5ede3..1787d8fc5 100644 --- a/src/testSetup.ts +++ b/src/testSetup.ts @@ -33,7 +33,7 @@ defaultStrings['Maintainer'] = '{{maintainer}}-Default'; (window as any).GENERATORS = { eng: 'eng-gener', spa: 'spa-gener' }; // eslint-disable-next-line -(window as any).SPELLERS = { hin: "hin-spell", kaz: "kaz-spell"}; +(window as any).SPELLERS = { hin: 'hin-spell', kaz: 'kaz-spell' }; process.on('unhandledRejection', (err) => { // eslint-disable-next-line jest/no-jasmine-globals diff --git a/src/types.ts b/src/types.ts index 71576cbb6..f15b2c8b4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,7 +5,7 @@ export enum Mode { Analysis = 'analysis', Generation = 'generation', Sandbox = 'sandbox', - SpellChecker = 'spellchecker' + SpellChecker = 'spellchecker', } export type Config = { From b35ca825433022fc9aca0c4efdf7bc177cc7eeaf Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Fri, 21 Jun 2024 18:31:12 +0000 Subject: [PATCH 09/26] fixng styling --- src/components/spellchecker/SpellChecker.tsx | 144 +++++++++++-------- src/components/spellchecker/spellchecker.css | 82 +++++------ 2 files changed, 117 insertions(+), 109 deletions(-) diff --git a/src/components/spellchecker/SpellChecker.tsx b/src/components/spellchecker/SpellChecker.tsx index 6f3a5bcdc..99585ce51 100644 --- a/src/components/spellchecker/SpellChecker.tsx +++ b/src/components/spellchecker/SpellChecker.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import Button from 'react-bootstrap/Button'; import Col from 'react-bootstrap/Col'; import Form from 'react-bootstrap/Form'; @@ -25,27 +25,11 @@ const SpellChecker = (): React.ReactElement => { const history = useHistory(); const { t, tLang } = useLocalization(); const { trackEvent } = useMatomo(); - const [suggestions, setSuggestions] = React.useState([ - { - token: 'Thiss', - known: false, - sugg: [ - ['This', 0.9], - ['Thus', 0.1], - ], - }, - { - token: 'exampel', - known: false, - sugg: [ - ['example', 0.95], - ['exemplar', 0.05], - ], - }, - ]); - const [selectedWord, setSelectedWord] = React.useState(null); - const [loading, setLoading] = React.useState(false); - const [error, setError] = React.useState(null); + const [suggestions, setSuggestions] = useState([]); + const [selectedWord, setSelectedWord] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [suggestionPosition, setSuggestionPosition] = useState<{ top: number, left: number } | null>(null); const [lang, setLang] = useLocalStorage('spellerLang', Object.keys(Spellers)[0], { overrideValue: toAlpha3Code(getUrlParam(history.location.search, langUrlParam)), @@ -56,7 +40,7 @@ const SpellChecker = (): React.ReactElement => { overrideValue: getUrlParam(history.location.search, textUrlParam), }); - React.useEffect(() => { + useEffect(() => { let search = buildNewSearch({ [langUrlParam]: lang, [textUrlParam]: text }); if (search.length > MaxURLLength) { search = buildNewSearch({ [langUrlParam]: lang }); @@ -64,57 +48,91 @@ const SpellChecker = (): React.ReactElement => { history.replace({ search }); }, [history, lang, text]); - const spellcheckRef = React.useRef(null); + const spellcheckRef = useRef(null); + + const handleSubmit = () => { + if (text.trim().length === 0) { + return; + } + + // Simulating the API call + setLoading(true); + setTimeout(() => { + setSuggestions(checkSpelling(text)); + setError(null); + setLoading(false); + }, 1000); + }; const handleInput = (e: React.FormEvent) => { setText(e.currentTarget.innerText); }; - const handleWordClick = (word: string) => { - console.log("Word clicked:", word); + const handleWordClick = (word: string, event: React.MouseEvent) => { setSelectedWord(word); + const rect = event.currentTarget.getBoundingClientRect(); + setSuggestionPosition({ + top: rect.bottom + window.scrollY + 3, // Adjust for scrolling + left: rect.left + window.scrollX -2, + }); }; const applySuggestion = (suggestion: string) => { if (!selectedWord) return; - const updatedText = text.replace(new RegExp(`\\b${selectedWord}\\b`, 'g'), suggestion); - setText(updatedText); // Schedule state update - - console.log(text) + setText(updatedText); setSelectedWord(null); - renderHighlightedText() + + // Remove the applied suggestion from the suggestions list + setSuggestions((prevSuggestions) => + prevSuggestions.filter((s) => s.token !== selectedWord) + ); }; - const renderHighlightedText = () => { - if (text.trim().length === 0) { - return; - } - console.log(text) - console.log("yo i got called!!!") - - const contentElement = spellcheckRef.current; - if (contentElement instanceof HTMLElement) { - - const parts = text.split(/(\s+)/).map((word, index) => { - const suggestion = suggestions.find((s) => s.token === word && !s.known); - if (suggestion) { - return `${word}`; - } else { - return `${word}`; - } - }).join(''); - - contentElement.innerHTML = parts; - - const misspelledElements = contentElement.querySelectorAll('.misspelled'); - misspelledElements.forEach((element, index) => { - const word = element.textContent || ''; - element.addEventListener('click', () => handleWordClick(word)); - }); - } + const checkSpelling = (inputText: string): Suggestion[] => { + // Simulated response from the spell checker + return [ + { + token: 'Thiss', + known: false, + sugg: [ + ['This', 0.9], + ['Thus', 0.1], + ['Thus', 0.1], + ['Thus', 0.1], + ['Thus', 0.1], + ['Thus', 0.1], + ['Thus', 0.1], + ['Thus', 0.1], + ['Thus', 0.1], + ], + }, + { + token: 'exampel', + known: false, + sugg: [ + ['example', 0.95], + ['exemplar', 0.05], + ], + }, + ]; }; + const renderHighlightedText = () => { + const parts = text.split(/(\s+)/).map((word, index) => { + const suggestion = suggestions.find((s) => s.token === word && !s.known); + if (suggestion) { + return ( + handleWordClick(word, e)}> + {word} + + ); + } + return word; + }); + return <>{parts}; + } + return (
event.preventDefault()}> @@ -142,18 +160,20 @@ const SpellChecker = (): React.ReactElement => { contentEditable ref={spellcheckRef} onInput={handleInput} - /> + > + {renderHighlightedText()} + - - {selectedWord && ( -
+ {selectedWord && suggestionPosition && ( +
{suggestions .find((s) => s.token === selectedWord) ?.sugg.map(([sugg, _], index) => ( diff --git a/src/components/spellchecker/spellchecker.css b/src/components/spellchecker/spellchecker.css index a3c7fda74..9eacb60ac 100644 --- a/src/components/spellchecker/spellchecker.css +++ b/src/components/spellchecker/spellchecker.css @@ -1,49 +1,37 @@ .content-editable { - border: 1px solid #ccc; - padding: 10px; - min-height: 150px; - white-space: pre-wrap; - word-wrap: break-word; - } - - .misspelled { - position: relative; - display: inline-block; - text-decoration: underline wavy red; - cursor: pointer; - } - - .suggestions { - background: white; - border: 1px solid #ccc; - max-height: 150px; - overflow-y: auto; - text-align: center; - border-radius: 6px; - padding: 5px 0; - position: absolute; - z-index: 1; - top: 150%; - left: 50%; - margin-left: -60px; - } - .suggestions::after { - content: ""; - position: absolute; - bottom: 100%; - left: 50%; - margin-left: -5px; - border-width: 5px; - border-style: solid; - border-color: transparent transparent black transparent; - } + border: 1px solid #ccc; + padding: 10px; + min-height: 150px; + white-space: pre-wrap; + word-wrap: break-word; +} - .suggestions div { - padding: 5px; - cursor: pointer; - } - - .suggestions div:hover { - background-color: #f0f0f0; - } - \ No newline at end of file +.misspelled { + text-decoration: underline wavy red; + cursor: pointer; +} + +.suggestions { + text-align: center; + position: absolute; + background: white; + border: 1px solid #ccc; + z-index: 1000; + max-height: 150px; + overflow-y: auto; + white-space: nowrap; + width: auto; /* Adjusted width */ + padding: 5px; + border-radius: 6px; + box-shadow: 0px 0px 10px rgba(0,0,0,0.1); /* Optional: for a slight shadow */ +; +} + +.suggestions div { + padding: 5px; + cursor: pointer; +} + +.suggestions div:hover { + background-color: #f0f0f0; +} From 45855bb163a0bc0b1108dfa6adb20f4eb71399e1 Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Sat, 22 Jun 2024 06:54:54 +0000 Subject: [PATCH 10/26] fixing issue with state updates with renderHighlightText --- src/components/spellchecker/SpellChecker.tsx | 168 +++++++++---------- src/components/spellchecker/spellchecker.css | 6 +- 2 files changed, 84 insertions(+), 90 deletions(-) diff --git a/src/components/spellchecker/SpellChecker.tsx b/src/components/spellchecker/SpellChecker.tsx index 99585ce51..0054dab11 100644 --- a/src/components/spellchecker/SpellChecker.tsx +++ b/src/components/spellchecker/SpellChecker.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect } from 'react'; +import * as React from 'react'; import Button from 'react-bootstrap/Button'; import Col from 'react-bootstrap/Col'; import Form from 'react-bootstrap/Form'; @@ -25,11 +25,28 @@ const SpellChecker = (): React.ReactElement => { const history = useHistory(); const { t, tLang } = useLocalization(); const { trackEvent } = useMatomo(); - const [suggestions, setSuggestions] = useState([]); - const [selectedWord, setSelectedWord] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [suggestionPosition, setSuggestionPosition] = useState<{ top: number, left: number } | null>(null); + const [suggestions, setSuggestions] = React.useState([ + { + token: 'Thiss', + known: false, + sugg: [ + ['This', 0.9], + ['Thus', 0.1], + ], + }, + { + token: 'exampel', + known: false, + sugg: [ + ['example', 0.95], + ['exemplar', 0.05], + ], + }, + ]); + const [selectedWord, setSelectedWord] = React.useState(null); + const [loading, setLoading] = React.useState(false); + const [error, setError] = React.useState(null); + const [suggestionPosition, setSuggestionPosition] = React.useState<{ top: number; left: number } | null>(null); const [lang, setLang] = useLocalStorage('spellerLang', Object.keys(Spellers)[0], { overrideValue: toAlpha3Code(getUrlParam(history.location.search, langUrlParam)), @@ -40,7 +57,7 @@ const SpellChecker = (): React.ReactElement => { overrideValue: getUrlParam(history.location.search, textUrlParam), }); - useEffect(() => { + React.useEffect(() => { let search = buildNewSearch({ [langUrlParam]: lang, [textUrlParam]: text }); if (search.length > MaxURLLength) { search = buildNewSearch({ [langUrlParam]: lang }); @@ -48,91 +65,74 @@ const SpellChecker = (): React.ReactElement => { history.replace({ search }); }, [history, lang, text]); - const spellcheckRef = useRef(null); + React.useEffect(() => { + renderHighlightedText(text); + }, []); - const handleSubmit = () => { - if (text.trim().length === 0) { - return; - } - - // Simulating the API call - setLoading(true); - setTimeout(() => { - setSuggestions(checkSpelling(text)); - setError(null); - setLoading(false); - }, 1000); - }; + const spellcheckRef = React.useRef(null); const handleInput = (e: React.FormEvent) => { setText(e.currentTarget.innerText); }; - const handleWordClick = (word: string, event: React.MouseEvent) => { + const handleWordClick = React.useCallback((word: string, event: MouseEvent | TouchEvent) => { setSelectedWord(word); - const rect = event.currentTarget.getBoundingClientRect(); - setSuggestionPosition({ - top: rect.bottom + window.scrollY + 3, // Adjust for scrolling - left: rect.left + window.scrollX -2, - }); - }; + const rect = (event.currentTarget as Element).getBoundingClientRect(); + + if ('touches' in event) { + // Get the first touch point + const touch = event.touches[0]; + setSuggestionPosition({ + top: rect.bottom + window.scrollY + 3, + left: touch.clientX + window.scrollX - 2, + }); + } else { + setSuggestionPosition({ + top: rect.bottom + window.scrollY + 3, + left: rect.left + window.scrollX - 2, + }); + } + }, []); + + const renderHighlightedText = React.useCallback((text: string) => { + if (text.trim().length === 0) { + return; + } + + const contentElement = spellcheckRef.current; + if (contentElement instanceof HTMLElement) { + const parts = text.split(/(\s+)/).map((word, index) => { + const suggestion = suggestions.find((s) => s.token === word && !s.known); + if (suggestion) { + return `${word}`; + } else { + return `${word}`; + } + }).join(''); + + contentElement.innerHTML = parts; + + const misspelledElements = contentElement.querySelectorAll('.misspelled'); + misspelledElements.forEach((element, index) => { + const word = element.textContent || ''; + const eventHandler = (e: Event) => handleWordClick(word, e as MouseEvent | TouchEvent); + element.addEventListener('click', eventHandler); + element.addEventListener('touchstart', eventHandler); + }); + } + }, [text, suggestions, handleWordClick]); const applySuggestion = (suggestion: string) => { if (!selectedWord) return; - const updatedText = text.replace(new RegExp(`\\b${selectedWord}\\b`, 'g'), suggestion); - setText(updatedText); - setSelectedWord(null); - // Remove the applied suggestion from the suggestions list - setSuggestions((prevSuggestions) => - prevSuggestions.filter((s) => s.token !== selectedWord) - ); - }; + const updatedText = text.replace(new RegExp(`\\b${selectedWord}\\b`, 'g'), suggestion); + setText(updatedText); - const checkSpelling = (inputText: string): Suggestion[] => { - // Simulated response from the spell checker - return [ - { - token: 'Thiss', - known: false, - sugg: [ - ['This', 0.9], - ['Thus', 0.1], - ['Thus', 0.1], - ['Thus', 0.1], - ['Thus', 0.1], - ['Thus', 0.1], - ['Thus', 0.1], - ['Thus', 0.1], - ['Thus', 0.1], - ], - }, - { - token: 'exampel', - known: false, - sugg: [ - ['example', 0.95], - ['exemplar', 0.05], - ], - }, - ]; + console.log(text); + setSelectedWord(null); + renderHighlightedText(updatedText); }; - const renderHighlightedText = () => { - const parts = text.split(/(\s+)/).map((word, index) => { - const suggestion = suggestions.find((s) => s.token === word && !s.known); - if (suggestion) { - return ( - handleWordClick(word, e)}> - {word} - - ); - } - return word; - }); - return <>{parts}; - } - return ( event.preventDefault()}> @@ -141,9 +141,7 @@ const SpellChecker = (): React.ReactElement => { setLang(value)} required value={lang}> {Object.keys(Spellers) .map((code) => [code, tLang(code)]) - .sort(([, a], [, b]) => { - return a.toLowerCase().localeCompare(b.toLowerCase()); - }) + .sort(([, a], [, b]) => a.toLowerCase().localeCompare(b.toLowerCase())) .map(([code, name]) => (
+ /> - {selectedWord && suggestionPosition && ( -
+
{suggestions .find((s) => s.token === selectedWord) ?.sugg.map(([sugg, _], index) => ( diff --git a/src/components/spellchecker/spellchecker.css b/src/components/spellchecker/spellchecker.css index 9eacb60ac..1be3d5da1 100644 --- a/src/components/spellchecker/spellchecker.css +++ b/src/components/spellchecker/spellchecker.css @@ -8,7 +8,6 @@ .misspelled { text-decoration: underline wavy red; - cursor: pointer; } .suggestions { @@ -20,11 +19,10 @@ max-height: 150px; overflow-y: auto; white-space: nowrap; - width: auto; /* Adjusted width */ + width: auto; padding: 5px; border-radius: 6px; - box-shadow: 0px 0px 10px rgba(0,0,0,0.1); /* Optional: for a slight shadow */ -; + box-shadow: 0px 0px 10px rgba(0,0,0,0.1); } .suggestions div { From f91abdd51ab3c2767a9396e7e0e041ff7bbe3343 Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Sat, 22 Jun 2024 11:12:37 +0000 Subject: [PATCH 11/26] adding handleSubmit function --- src/components/spellchecker/SpellChecker.tsx | 37 +++++++++++++++++--- src/components/spellchecker/spellchecker.css | 1 + 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/components/spellchecker/SpellChecker.tsx b/src/components/spellchecker/SpellChecker.tsx index 0054dab11..2c06e73bd 100644 --- a/src/components/spellchecker/SpellChecker.tsx +++ b/src/components/spellchecker/SpellChecker.tsx @@ -16,6 +16,7 @@ interface Suggestion { sugg: Array<[string, number]>; } +// eslint-disable-next-line const Spellers: Readonly> = (window as any).SPELLERS; const langUrlParam = 'lang'; @@ -44,7 +45,6 @@ const SpellChecker = (): React.ReactElement => { }, ]); const [selectedWord, setSelectedWord] = React.useState(null); - const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState(null); const [suggestionPosition, setSuggestionPosition] = React.useState<{ top: number; left: number } | null>(null); @@ -113,7 +113,7 @@ const SpellChecker = (): React.ReactElement => { contentElement.innerHTML = parts; const misspelledElements = contentElement.querySelectorAll('.misspelled'); - misspelledElements.forEach((element, index) => { + misspelledElements.forEach((element) => { const word = element.textContent || ''; const eventHandler = (e: Event) => handleWordClick(word, e as MouseEvent | TouchEvent); element.addEventListener('click', eventHandler); @@ -127,8 +127,6 @@ const SpellChecker = (): React.ReactElement => { const updatedText = text.replace(new RegExp(`\\b${selectedWord}\\b`, 'g'), suggestion); setText(updatedText); - - console.log(text); setSelectedWord(null); renderHighlightedText(updatedText); }; @@ -184,3 +182,34 @@ const SpellChecker = (): React.ReactElement => { }; export default SpellChecker; + + + + +const handleSubmit = () => { + if (text.trim().length === 0) { + return; + } + + spellcheckResult.current?.cancel(); + spellcheckResult.current = null; + + void (async () => { + try { + trackEvent({ category: 'spellchecker', action: 'spellcheck', name: lang, value: text.length }); + const [ref, request] = apyFetch('speller', { lang, q: text }); + spellcheckResult.current = ref; + setSuggestions((await request).data as Array); + setError(null); + spellcheckResult.current = null; + + renderHighlightedText(text); + + } catch (error) { + if (!axios.isCancel(error)) { + setSuggestions([]); + setError(error as Error); + } + } + })(); +}; \ No newline at end of file diff --git a/src/components/spellchecker/spellchecker.css b/src/components/spellchecker/spellchecker.css index 1be3d5da1..063ee744b 100644 --- a/src/components/spellchecker/spellchecker.css +++ b/src/components/spellchecker/spellchecker.css @@ -8,6 +8,7 @@ .misspelled { text-decoration: underline wavy red; + cursor: pointer; } .suggestions { From 7c214736e91257d787ae460d9ef8bfb6a08d2ad3 Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Sat, 22 Jun 2024 14:12:22 +0000 Subject: [PATCH 12/26] refactoring SpellCheck component --- src/App.tsx | 9 +- src/components/spellchecker/SpellChecker.tsx | 188 +++++++++++-------- src/components/spellchecker/spellchecker.css | 4 +- src/strings/eng.json | 3 +- 4 files changed, 111 insertions(+), 93 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 79d788755..cecb35a78 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,13 +14,13 @@ import { langDirection, toAlpha2Code } from './util/languages'; import { Mode } from './types'; import Analyzer from './components/Analyzer'; -import SpellChecker from './components/spellchecker/SpellChecker'; import { Path as DocTranslationPath } from './components/translator/DocTranslationForm'; import Footer from './components/footer'; import Generator from './components/Generator'; import LocaleSelector from './components/LocaleSelector'; import Navbar from './components/navbar'; import Sandbox from './components/Sandbox'; +import SpellChecker from './components/spellchecker/SpellChecker'; import Translator from './components/translator/Translator'; import { Mode as TranslatorMode } from './components/translator'; import { Path as WebpageTranslationPath } from './components/translator/WebpageTranslationForm'; @@ -129,11 +129,6 @@ const App = ({ setLocale }: { setLocale: React.Dispatch )} - {enabledModes.has(Mode.SpellChecker) && ( - - // write your spellchecker here - - )}
@@ -153,4 +148,4 @@ const ConnectedApp: React.VoidFunctionComponent = () => ( ); -export default ConnectedApp; +export default ConnectedApp; \ No newline at end of file diff --git a/src/components/spellchecker/SpellChecker.tsx b/src/components/spellchecker/SpellChecker.tsx index 2c06e73bd..016433480 100644 --- a/src/components/spellchecker/SpellChecker.tsx +++ b/src/components/spellchecker/SpellChecker.tsx @@ -1,14 +1,18 @@ import * as React from 'react'; +import axios, { CancelTokenSource } from 'axios'; import Button from 'react-bootstrap/Button'; import Col from 'react-bootstrap/Col'; import Form from 'react-bootstrap/Form'; import { useHistory } from 'react-router-dom'; import { useMatomo } from '@datapunt/matomo-tracker-react'; import { MaxURLLength, buildNewSearch, getUrlParam } from '../../util/url'; +import { APyContext } from '../../context'; +import classNames from 'classnames'; import { toAlpha3Code } from '../../util/languages'; import useLocalStorage from '../../util/useLocalStorage'; import { useLocalization } from '../../util/localization'; import './spellchecker.css'; +import ErrorAlert from '../ErrorAlert'; interface Suggestion { token: string; @@ -21,33 +25,24 @@ const Spellers: Readonly> = (window as any).SPELLERS; const langUrlParam = 'lang'; const textUrlParam = 'q'; - -const SpellChecker = (): React.ReactElement => { +const SpellCheckForm = ({ + setLoading, + setError, +}: { + setLoading: React.Dispatch>; + setError: React.Dispatch>; +}): React.ReactElement => { const history = useHistory(); const { t, tLang } = useLocalization(); const { trackEvent } = useMatomo(); - const [suggestions, setSuggestions] = React.useState([ - { - token: 'Thiss', - known: false, - sugg: [ - ['This', 0.9], - ['Thus', 0.1], - ], - }, - { - token: 'exampel', - known: false, - sugg: [ - ['example', 0.95], - ['exemplar', 0.05], - ], - }, - ]); + const apyFetch = React.useContext(APyContext); + const [suggestions, setSuggestions] = React.useState([]); const [selectedWord, setSelectedWord] = React.useState(null); - const [error, setError] = React.useState(null); const [suggestionPosition, setSuggestionPosition] = React.useState<{ top: number; left: number } | null>(null); + const spellcheckRef = React.useRef(null); + const spellcheckResult = React.useRef(null); + const [lang, setLang] = useLocalStorage('spellerLang', Object.keys(Spellers)[0], { overrideValue: toAlpha3Code(getUrlParam(history.location.search, langUrlParam)), validateValue: (l) => l in Spellers, @@ -66,22 +61,49 @@ const SpellChecker = (): React.ReactElement => { }, [history, lang, text]); React.useEffect(() => { - renderHighlightedText(text); + renderHighlightedText(text); }, []); - const spellcheckRef = React.useRef(null); - const handleInput = (e: React.FormEvent) => { setText(e.currentTarget.innerText); }; + const handleSubmit = () => { + if (text.trim().length === 0) { + return; + } + + spellcheckResult.current?.cancel(); + spellcheckResult.current = null; + + void (async () => { + try { + trackEvent({ category: 'spellchecker', action: 'spellcheck', name: lang, value: text.length }); + const [ref, request] = apyFetch('speller', { lang, q: text }); + spellcheckResult.current = ref; + setSuggestions((await request).data as Array); + setError(null); + spellcheckResult.current = null; + + renderHighlightedText(text); + setLoading(false); + } catch (error) { + if (!axios.isCancel(error)) { + setSuggestions([]); + setError(error as Error); + setLoading(false); + } + } + })(); + }; + const handleWordClick = React.useCallback((word: string, event: MouseEvent | TouchEvent) => { setSelectedWord(word); const rect = (event.currentTarget as Element).getBoundingClientRect(); if ('touches' in event) { // Get the first touch point - const touch = event.touches[0]; + const touch = event.touches[0]; setSuggestionPosition({ top: rect.bottom + window.scrollY + 3, left: touch.clientX + window.scrollX - 2, @@ -94,39 +116,45 @@ const SpellChecker = (): React.ReactElement => { } }, []); - const renderHighlightedText = React.useCallback((text: string) => { - if (text.trim().length === 0) { - return; - } - - const contentElement = spellcheckRef.current; - if (contentElement instanceof HTMLElement) { - const parts = text.split(/(\s+)/).map((word, index) => { - const suggestion = suggestions.find((s) => s.token === word && !s.known); - if (suggestion) { - return `${word}`; - } else { - return `${word}`; - } - }).join(''); - - contentElement.innerHTML = parts; + const renderHighlightedText = React.useCallback( + (text: string) => { + if (text.trim().length === 0) { + return; + } - const misspelledElements = contentElement.querySelectorAll('.misspelled'); - misspelledElements.forEach((element) => { - const word = element.textContent || ''; - const eventHandler = (e: Event) => handleWordClick(word, e as MouseEvent | TouchEvent); - element.addEventListener('click', eventHandler); - element.addEventListener('touchstart', eventHandler); - }); - } - }, [text, suggestions, handleWordClick]); + const contentElement = spellcheckRef.current; + if (contentElement instanceof HTMLElement) { + const parts = text + .split(/(\s+)/) + .map((word, index) => { + const suggestion = suggestions.find((s) => s.token === word && !s.known); + if (suggestion) { + return `${word}`; + } else { + return `${word}`; + } + }) + .join(''); + + contentElement.innerHTML = parts; + + const misspelledElements = contentElement.querySelectorAll('.misspelled'); + misspelledElements.forEach((element) => { + const word = element.textContent || ''; + const eventHandler = (e: Event) => handleWordClick(word, e as MouseEvent | TouchEvent); + element.addEventListener('click', eventHandler); + element.addEventListener('touchstart', eventHandler); + }); + } + }, + [suggestions, handleWordClick], + ); const applySuggestion = (suggestion: string) => { if (!selectedWord) return; const updatedText = text.replace(new RegExp(`\\b${selectedWord}\\b`, 'g'), suggestion); - setText(updatedText); + setText(updatedText); setSelectedWord(null); renderHighlightedText(updatedText); }; @@ -154,23 +182,33 @@ const SpellChecker = (): React.ReactElement => {
) => { + if (event.code === 'Enter' && !event.shiftKey) { + event.preventDefault(); + handleSubmit(); + } + }} + placeholder={t('Spell_Checking_Help')} + ref={spellcheckRef} /> - {selectedWord && suggestionPosition && ( -
+
{suggestions .find((s) => s.token === selectedWord) - ?.sugg.map(([sugg, _], index) => ( + ?.sugg.map(([sugg], index) => (
applySuggestion(sugg)}> {sugg}
@@ -181,35 +219,19 @@ const SpellChecker = (): React.ReactElement => { ); }; -export default SpellChecker; - - - +const SpellChecker = (): React.ReactElement => { + const [error, setError] = React.useState(null); + const [loading, setLoading] = React.useState(false); -const handleSubmit = () => { - if (text.trim().length === 0) { - return; - } + return ( + <> + +
{error && }
+ + ); +}; - spellcheckResult.current?.cancel(); - spellcheckResult.current = null; - void (async () => { - try { - trackEvent({ category: 'spellchecker', action: 'spellcheck', name: lang, value: text.length }); - const [ref, request] = apyFetch('speller', { lang, q: text }); - spellcheckResult.current = ref; - setSuggestions((await request).data as Array); - setError(null); - spellcheckResult.current = null; +export default SpellChecker; - renderHighlightedText(text); - } catch (error) { - if (!axios.isCancel(error)) { - setSuggestions([]); - setError(error as Error); - } - } - })(); -}; \ No newline at end of file diff --git a/src/components/spellchecker/spellchecker.css b/src/components/spellchecker/spellchecker.css index 063ee744b..64fd27df0 100644 --- a/src/components/spellchecker/spellchecker.css +++ b/src/components/spellchecker/spellchecker.css @@ -20,10 +20,10 @@ max-height: 150px; overflow-y: auto; white-space: nowrap; - width: auto; + width: auto; padding: 5px; border-radius: 6px; - box-shadow: 0px 0px 10px rgba(0,0,0,0.1); + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1); } .suggestions div { diff --git a/src/strings/eng.json b/src/strings/eng.json index fbf728ab7..ba0128327 100644 --- a/src/strings/eng.json +++ b/src/strings/eng.json @@ -65,5 +65,6 @@ "Contact_Para": "

IRC channel

The quickest way to contact us is by joining our IRC channel, #apertium at irc.oftc.net, where users and developers of Apertium meet. You don't need an IRC client; you can use OFTC webchat.

Mailing list

Also, subscribe to the apertium-stuff mailing list, where you can post longer proposals or issues, as well as follow general Apertium discussions.

Contact

Feel free to contact us via the apertium-contact mailing list if you find a mistake, there's a project you would like to see us work on, or you would like to help out.

", "Install_Apertium": "Install Apertium", "Install_Apertium_Para": "Are you experiencing slow responses? Our servers might be overloaded. Learn how to install Apertium locally.", - "Spell_Check" : "Spell Checker" + "Spell_Check" : "Spell Checker", + "Spell_Checking_Help" : "Enter text for spell checking." } From 61fb332c95cf21b22a5ece824a6d7432b8f02f9c Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Sat, 22 Jun 2024 17:09:06 +0000 Subject: [PATCH 13/26] removing unnecessary script --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index ee6c7a5ce..34b7ea24d 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,6 @@ "author": "Sushain Cherivirala ", "license": "GPL-3.0-or-later", "scripts": { - "start": "yarn build && yarn serve", "build": "ts-node build.ts", "serve": "python3 -m http.server --directory dist", "tsc": "tsc --noEmit", From 554f45521a2a0acb916332657d8887e4ecbb974c Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Fri, 28 Jun 2024 06:39:59 +0000 Subject: [PATCH 14/26] trying to fix eslint issues --- src/App.tsx | 2 +- src/components/spellchecker/SpellChecker.tsx | 41 +++++++++++++------- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index cecb35a78..9c798a6e7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -148,4 +148,4 @@ const ConnectedApp: React.VoidFunctionComponent = () => ( ); -export default ConnectedApp; \ No newline at end of file +export default ConnectedApp; diff --git a/src/components/spellchecker/SpellChecker.tsx b/src/components/spellchecker/SpellChecker.tsx index 016433480..36fbd3a94 100644 --- a/src/components/spellchecker/SpellChecker.tsx +++ b/src/components/spellchecker/SpellChecker.tsx @@ -1,18 +1,22 @@ +import './spellchecker.css'; import * as React from 'react'; import axios, { CancelTokenSource } from 'axios'; import Button from 'react-bootstrap/Button'; import Col from 'react-bootstrap/Col'; import Form from 'react-bootstrap/Form'; +import classNames from 'classnames'; import { useHistory } from 'react-router-dom'; import { useMatomo } from '@datapunt/matomo-tracker-react'; -import { MaxURLLength, buildNewSearch, getUrlParam } from '../../util/url'; + import { APyContext } from '../../context'; -import classNames from 'classnames'; import { toAlpha3Code } from '../../util/languages'; + +import { MaxURLLength, buildNewSearch, getUrlParam } from '../../util/url'; +import ErrorAlert from '../ErrorAlert'; import useLocalStorage from '../../util/useLocalStorage'; import { useLocalization } from '../../util/localization'; -import './spellchecker.css'; -import ErrorAlert from '../ErrorAlert'; + +// import XRegExp from 'xregexp'; interface Suggestion { token: string; @@ -62,7 +66,7 @@ const SpellCheckForm = ({ React.useEffect(() => { renderHighlightedText(text); - }, []); + }); const handleInput = (e: React.FormEvent) => { setText(e.currentTarget.innerText); @@ -153,10 +157,13 @@ const SpellCheckForm = ({ const applySuggestion = (suggestion: string) => { if (!selectedWord) return; - const updatedText = text.replace(new RegExp(`\\b${selectedWord}\\b`, 'g'), suggestion); - setText(updatedText); - setSelectedWord(null); - renderHighlightedText(updatedText); + // const regex = XRegExp(`(^|\\s)${XRegExp.escape(selectedWord)}(?=\\s|$)`, 'g'); + // const updatedText = XRegExp.replace(text, regex, `$1${suggestion}`); + + // setText(updatedText); + + // setSelectedWord(null); + // renderHighlightedText(updatedText); }; return ( @@ -191,6 +198,8 @@ const SpellCheckForm = ({ }} placeholder={t('Spell_Checking_Help')} ref={spellcheckRef} + role="textbox" + tabIndex={0} /> @@ -209,7 +218,16 @@ const SpellCheckForm = ({ {suggestions .find((s) => s.token === selectedWord) ?.sugg.map(([sugg], index) => ( -
applySuggestion(sugg)}> +
applySuggestion(sugg)} + onKeyDown={(event) => { + if (event.key === 'Enter') { + applySuggestion(sugg); + } + }} + role="presentation" + > {sugg}
))} @@ -231,7 +249,4 @@ const SpellChecker = (): React.ReactElement => { ); }; - export default SpellChecker; - - From 30a28478cd729d12764a154d33b8058e094ba944 Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Fri, 28 Jun 2024 08:49:43 +0000 Subject: [PATCH 15/26] fixed lint errors --- src/components/spellchecker/SpellChecker.tsx | 14 ++++++-------- src/components/spellchecker/spellchecker.css | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/components/spellchecker/SpellChecker.tsx b/src/components/spellchecker/SpellChecker.tsx index 36fbd3a94..db5d81ec5 100644 --- a/src/components/spellchecker/SpellChecker.tsx +++ b/src/components/spellchecker/SpellChecker.tsx @@ -16,8 +16,6 @@ import ErrorAlert from '../ErrorAlert'; import useLocalStorage from '../../util/useLocalStorage'; import { useLocalization } from '../../util/localization'; -// import XRegExp from 'xregexp'; - interface Suggestion { token: string; known: boolean; @@ -157,13 +155,13 @@ const SpellCheckForm = ({ const applySuggestion = (suggestion: string) => { if (!selectedWord) return; - // const regex = XRegExp(`(^|\\s)${XRegExp.escape(selectedWord)}(?=\\s|$)`, 'g'); - // const updatedText = XRegExp.replace(text, regex, `$1${suggestion}`); - - // setText(updatedText); + const escapedSelectedWord = selectedWord.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&'); + const regex = new RegExp(`(^|\\s)${escapedSelectedWord}(?=\\s|$)`, 'gu'); + const updatedText = text.replace(regex, (p1) => `${p1}${suggestion}`); - // setSelectedWord(null); - // renderHighlightedText(updatedText); + setText(updatedText); + setSelectedWord(null); + renderHighlightedText(updatedText); }; return ( diff --git a/src/components/spellchecker/spellchecker.css b/src/components/spellchecker/spellchecker.css index 64fd27df0..1ffb85ca7 100644 --- a/src/components/spellchecker/spellchecker.css +++ b/src/components/spellchecker/spellchecker.css @@ -23,7 +23,7 @@ width: auto; padding: 5px; border-radius: 6px; - box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1); + box-shadow: 0 0 10px rgb(0 0 0 / 10%); } .suggestions div { From 1dae8d2c0252d6ba75bb838c4f8b89d22f335257 Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Fri, 28 Jun 2024 09:05:18 +0000 Subject: [PATCH 16/26] fixed a failing test case --- src/components/navbar/__tests__/index.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/navbar/__tests__/index.test.tsx b/src/components/navbar/__tests__/index.test.tsx index 4b4ff7c59..dcec6f0a8 100644 --- a/src/components/navbar/__tests__/index.test.tsx +++ b/src/components/navbar/__tests__/index.test.tsx @@ -33,7 +33,7 @@ describe('navigation options', () => { const navbar = screen.getByTestId('navbar-mobile'); const links = getAllByRole(navbar, 'link', { name: (n) => n !== 'Toggle navigation' }); - expect(links).toHaveLength(4); + expect(links).toHaveLength(5); }); it('includes button', () => { From 23b45976a3d62647a35fc01819ebeee88e98031b Mon Sep 17 00:00:00 2001 From: satti-hari-krishna-reddy Date: Fri, 28 Jun 2024 16:15:18 +0530 Subject: [PATCH 17/26] fixing the text not showing up from local storage --- src/components/spellchecker/SpellChecker.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/spellchecker/SpellChecker.tsx b/src/components/spellchecker/SpellChecker.tsx index db5d81ec5..9d8b30071 100644 --- a/src/components/spellchecker/SpellChecker.tsx +++ b/src/components/spellchecker/SpellChecker.tsx @@ -41,7 +41,7 @@ const SpellCheckForm = ({ const [suggestions, setSuggestions] = React.useState([]); const [selectedWord, setSelectedWord] = React.useState(null); const [suggestionPosition, setSuggestionPosition] = React.useState<{ top: number; left: number } | null>(null); - + const initialRender = React.useRef(true); const spellcheckRef = React.useRef(null); const spellcheckResult = React.useRef(null); @@ -62,10 +62,6 @@ const SpellCheckForm = ({ history.replace({ search }); }, [history, lang, text]); - React.useEffect(() => { - renderHighlightedText(text); - }); - const handleInput = (e: React.FormEvent) => { setText(e.currentTarget.innerText); }; @@ -118,6 +114,11 @@ const SpellCheckForm = ({ } }, []); + if (initialRender.current && spellcheckRef.current) { + spellcheckRef.current.textContent = text; + initialRender.current = false; + } + const renderHighlightedText = React.useCallback( (text: string) => { if (text.trim().length === 0) { From 55870926985c922113ff461513bcea2563fda9b5 Mon Sep 17 00:00:00 2001 From: satti-hari-krishna-reddy Date: Fri, 28 Jun 2024 17:02:21 +0530 Subject: [PATCH 18/26] fixing prettier error and improving applySuggestion function --- src/components/spellchecker/SpellChecker.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/spellchecker/SpellChecker.tsx b/src/components/spellchecker/SpellChecker.tsx index 9d8b30071..24e486a5c 100644 --- a/src/components/spellchecker/SpellChecker.tsx +++ b/src/components/spellchecker/SpellChecker.tsx @@ -156,12 +156,13 @@ const SpellCheckForm = ({ const applySuggestion = (suggestion: string) => { if (!selectedWord) return; - const escapedSelectedWord = selectedWord.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&'); - const regex = new RegExp(`(^|\\s)${escapedSelectedWord}(?=\\s|$)`, 'gu'); - const updatedText = text.replace(regex, (p1) => `${p1}${suggestion}`); + const escapedSelectedWord: string = selectedWord.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&'); + const regex = new RegExp(`(^|\\s)${escapedSelectedWord}(?=\\s|$|[.,!?;:])`, 'gu'); + const updatedText = text.replace(regex, (match, p1: string) => `${p1}${suggestion}`); setText(updatedText); setSelectedWord(null); + console.log(updatedText); renderHighlightedText(updatedText); }; From bd0e4861d9d8d59410b520c954a23357abfbe978 Mon Sep 17 00:00:00 2001 From: satti-hari-krishna-reddy Date: Sat, 29 Jun 2024 14:50:54 +0530 Subject: [PATCH 19/26] saving and restoring caret position --- src/components/spellchecker/SpellChecker.tsx | 70 +++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/src/components/spellchecker/SpellChecker.tsx b/src/components/spellchecker/SpellChecker.tsx index 24e486a5c..97929c930 100644 --- a/src/components/spellchecker/SpellChecker.tsx +++ b/src/components/spellchecker/SpellChecker.tsx @@ -41,6 +41,7 @@ const SpellCheckForm = ({ const [suggestions, setSuggestions] = React.useState([]); const [selectedWord, setSelectedWord] = React.useState(null); const [suggestionPosition, setSuggestionPosition] = React.useState<{ top: number; left: number } | null>(null); + const initialRender = React.useRef(true); const spellcheckRef = React.useRef(null); const spellcheckResult = React.useRef(null); @@ -114,6 +115,69 @@ const SpellCheckForm = ({ } }, []); + function saveCaretPosition(editableDiv: HTMLElement): { start: number; end: number } | null { + if (window.getSelection) { + const sel = window.getSelection(); + if (sel && sel.rangeCount > 0) { + const range = sel.getRangeAt(0); + const preCaretRange = range.cloneRange(); + preCaretRange.selectNodeContents(editableDiv); + preCaretRange.setEnd(range.startContainer, range.startOffset); + const start = preCaretRange.toString().length; + const end = start + range.toString().length; + return { start, end }; + } + } + return null; + } + + function restoreCaretPosition(editableDiv: HTMLElement, savedSel: { start: number; end: number } | null) { + if (savedSel) { + const charIndex = { count: 0 }; + const range = document.createRange(); + range.setStart(editableDiv, 0); + range.collapse(true); + + const nodeStack: (ChildNode | HTMLElement)[] = [editableDiv]; + let node: ChildNode | null = null; + let foundStart = false; + let stop = false; + + while (!stop && (node = nodeStack.pop() || null)) { + if (node.nodeType === 3) { + // Text node + const textContent = node.textContent || ''; + const nextCharIndex = charIndex.count + textContent.length; + + if (!foundStart && savedSel.start >= charIndex.count && savedSel.start <= nextCharIndex) { + range.setStart(node, savedSel.start - charIndex.count); + foundStart = true; + } + + if (foundStart && savedSel.end >= charIndex.count && savedSel.end <= nextCharIndex) { + range.setEnd(node, savedSel.end - charIndex.count); + stop = true; + } + + charIndex.count = nextCharIndex; + } else { + const elementNode = node as HTMLElement; + let i = elementNode.childNodes.length; + + while (i--) { + nodeStack.push(elementNode.childNodes[i]); + } + } + } + + const sel = window.getSelection(); + if (sel) { + sel.removeAllRanges(); + sel.addRange(range); + } + } + } + if (initialRender.current && spellcheckRef.current) { spellcheckRef.current.textContent = text; initialRender.current = false; @@ -127,6 +191,7 @@ const SpellCheckForm = ({ const contentElement = spellcheckRef.current; if (contentElement instanceof HTMLElement) { + const savedSelection = saveCaretPosition(contentElement); const parts = text .split(/(\s+)/) .map((word, index) => { @@ -140,7 +205,6 @@ const SpellCheckForm = ({ .join(''); contentElement.innerHTML = parts; - const misspelledElements = contentElement.querySelectorAll('.misspelled'); misspelledElements.forEach((element) => { const word = element.textContent || ''; @@ -148,6 +212,7 @@ const SpellCheckForm = ({ element.addEventListener('click', eventHandler); element.addEventListener('touchstart', eventHandler); }); + restoreCaretPosition(contentElement, savedSelection); } }, [suggestions, handleWordClick], @@ -194,6 +259,9 @@ const SpellCheckForm = ({ if (event.code === 'Enter' && !event.shiftKey) { event.preventDefault(); handleSubmit(); + } + else if (event.code === 'Tab' || event.code === ' ') { + event.preventDefault(); } }} placeholder={t('Spell_Checking_Help')} From 5940faecf47f832e2dc6a26c2cd1a4996056be06 Mon Sep 17 00:00:00 2001 From: satti-hari-krishna-reddy Date: Fri, 26 Jul 2024 16:21:01 +0530 Subject: [PATCH 20/26] getting available voikko modes --- build.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.ts b/build.ts index 45d244594..14436ffe7 100755 --- a/build.ts +++ b/build.ts @@ -72,7 +72,7 @@ const Plugin = { apyGet('list', {}), apyGet('list', { q: 'analyzers' }), apyGet('list', { q: 'generators' }), - apyGet('list', { q: 'spellers' }), + apyGet('list', { q: 'voikko_modes' }), ]); } catch (error) { let message = new String(error).toString(); From 67971dcda28c51b27b2b3c84f75992bf43c74f65 Mon Sep 17 00:00:00 2001 From: satti-hari-krishna-reddy Date: Fri, 26 Jul 2024 16:22:52 +0530 Subject: [PATCH 21/26] making sure to strip way any html tags --- src/components/spellchecker/SpellChecker.tsx | 95 ++++++++++---------- 1 file changed, 46 insertions(+), 49 deletions(-) diff --git a/src/components/spellchecker/SpellChecker.tsx b/src/components/spellchecker/SpellChecker.tsx index 97929c930..ae3e6f811 100644 --- a/src/components/spellchecker/SpellChecker.tsx +++ b/src/components/spellchecker/SpellChecker.tsx @@ -19,7 +19,7 @@ import { useLocalization } from '../../util/localization'; interface Suggestion { token: string; known: boolean; - sugg: Array<[string, number]>; + sugg: Array; } // eslint-disable-next-line @@ -64,7 +64,8 @@ const SpellCheckForm = ({ }, [history, lang, text]); const handleInput = (e: React.FormEvent) => { - setText(e.currentTarget.innerText); + const plainText = e.currentTarget.innerText; + setText(plainText.replace(/<\/?[^>]+(>|$)/g, '')); // Strip away any HTML tags }; const handleSubmit = () => { @@ -78,13 +79,13 @@ const SpellCheckForm = ({ void (async () => { try { trackEvent({ category: 'spellchecker', action: 'spellcheck', name: lang, value: text.length }); - const [ref, request] = apyFetch('speller', { lang, q: text }); + const [ref, request] = apyFetch('speller/voikko', { lang, q: text }); spellcheckResult.current = ref; - setSuggestions((await request).data as Array); + const data = (await request).data as Array; + setSuggestions(data); + renderHighlightedText(text, data); setError(null); spellcheckResult.current = null; - - renderHighlightedText(text); setLoading(false); } catch (error) { if (!axios.isCancel(error)) { @@ -183,40 +184,38 @@ const SpellCheckForm = ({ initialRender.current = false; } - const renderHighlightedText = React.useCallback( - (text: string) => { - if (text.trim().length === 0) { - return; - } + const renderHighlightedText = (text: string, suggestions: Suggestion[]) => { + if (text.trim().length === 0) { + return; + } - const contentElement = spellcheckRef.current; - if (contentElement instanceof HTMLElement) { - const savedSelection = saveCaretPosition(contentElement); - const parts = text - .split(/(\s+)/) - .map((word, index) => { - const suggestion = suggestions.find((s) => s.token === word && !s.known); - if (suggestion) { - return `${word}`; - } else { - return `${word}`; - } - }) - .join(''); - - contentElement.innerHTML = parts; - const misspelledElements = contentElement.querySelectorAll('.misspelled'); - misspelledElements.forEach((element) => { - const word = element.textContent || ''; - const eventHandler = (e: Event) => handleWordClick(word, e as MouseEvent | TouchEvent); - element.addEventListener('click', eventHandler); - element.addEventListener('touchstart', eventHandler); - }); - restoreCaretPosition(contentElement, savedSelection); - } - }, - [suggestions, handleWordClick], - ); + const contentElement = spellcheckRef.current; + if (contentElement instanceof HTMLElement) { + const savedSelection = saveCaretPosition(contentElement); + + const parts = text + .split(/(\s+)/) + .map((word, index) => { + const suggestion = suggestions.find((s) => s.token === word && !s.known); + if (suggestion) { + return `${word}`; + } else { + return `${word}`; + } + }) + .join(''); + + contentElement.innerHTML = parts; + const misspelledElements = contentElement.querySelectorAll('.misspelled'); + misspelledElements.forEach((element) => { + const word = element.textContent || ''; + const eventHandler = (e: Event) => handleWordClick(word, e as MouseEvent | TouchEvent); + element.addEventListener('click', eventHandler); + element.addEventListener('touchstart', eventHandler); + }); + restoreCaretPosition(contentElement, savedSelection); + } + }; const applySuggestion = (suggestion: string) => { if (!selectedWord) return; @@ -227,8 +226,8 @@ const SpellCheckForm = ({ setText(updatedText); setSelectedWord(null); - console.log(updatedText); - renderHighlightedText(updatedText); + setSuggestionPosition(null); + renderHighlightedText(updatedText, suggestions); }; return ( @@ -252,6 +251,7 @@ const SpellCheckForm = ({ {t('Input_Text')}
{suggestions .find((s) => s.token === selectedWord) - ?.sugg.map(([sugg], index) => ( + ?.sugg.map((suggestion, index) => (
applySuggestion(sugg)} + onClick={() => applySuggestion(suggestion)} onKeyDown={(event) => { if (event.key === 'Enter') { - applySuggestion(sugg); + applySuggestion(suggestion); } }} role="presentation" > - {sugg} + {suggestion}
))}
From ad0fdb141c7f6a418caee3981043240967b27806 Mon Sep 17 00:00:00 2001 From: satti-hari-krishna-reddy Date: Sun, 28 Jul 2024 20:45:08 +0530 Subject: [PATCH 22/26] renaming the apy endpoint for spell checking --- build.ts | 2 +- src/components/spellchecker/SpellChecker.tsx | 2 +- src/testSetup.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.ts b/build.ts index 14436ffe7..87159d26e 100755 --- a/build.ts +++ b/build.ts @@ -72,7 +72,7 @@ const Plugin = { apyGet('list', {}), apyGet('list', { q: 'analyzers' }), apyGet('list', { q: 'generators' }), - apyGet('list', { q: 'voikko_modes' }), + apyGet('list', { q: 'spellcheckers' }), ]); } catch (error) { let message = new String(error).toString(); diff --git a/src/components/spellchecker/SpellChecker.tsx b/src/components/spellchecker/SpellChecker.tsx index ae3e6f811..196639d7e 100644 --- a/src/components/spellchecker/SpellChecker.tsx +++ b/src/components/spellchecker/SpellChecker.tsx @@ -79,7 +79,7 @@ const SpellCheckForm = ({ void (async () => { try { trackEvent({ category: 'spellchecker', action: 'spellcheck', name: lang, value: text.length }); - const [ref, request] = apyFetch('speller/voikko', { lang, q: text }); + const [ref, request] = apyFetch('spellCheck', { lang, q: text }); spellcheckResult.current = ref; const data = (await request).data as Array; setSuggestions(data); diff --git a/src/testSetup.ts b/src/testSetup.ts index 1787d8fc5..673b50292 100644 --- a/src/testSetup.ts +++ b/src/testSetup.ts @@ -33,7 +33,7 @@ defaultStrings['Maintainer'] = '{{maintainer}}-Default'; (window as any).GENERATORS = { eng: 'eng-gener', spa: 'spa-gener' }; // eslint-disable-next-line -(window as any).SPELLERS = { hin: 'hin-spell', kaz: 'kaz-spell' }; +(window as any).SPELLERS = { kaz: 'kaz-spell', hin: 'hin-spell' }; process.on('unhandledRejection', (err) => { // eslint-disable-next-line jest/no-jasmine-globals From bf50986a3bd4096a90ca3d1c81283520cd656a0d Mon Sep 17 00:00:00 2001 From: satti-hari-krishna-reddy Date: Sun, 28 Jul 2024 20:46:33 +0530 Subject: [PATCH 23/26] test(SpellChecker): add initial unit test cases --- .../__tests__/SpellChecker.test.tsx | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/components/spellchecker/__tests__/SpellChecker.test.tsx diff --git a/src/components/spellchecker/__tests__/SpellChecker.test.tsx b/src/components/spellchecker/__tests__/SpellChecker.test.tsx new file mode 100644 index 000000000..9944b4445 --- /dev/null +++ b/src/components/spellchecker/__tests__/SpellChecker.test.tsx @@ -0,0 +1,75 @@ +import * as React from 'react'; +import { MemoryHistory, MemoryHistoryBuildOptions, createMemoryHistory } from 'history'; +import { render, screen } from '@testing-library/react'; +import { Router } from 'react-router-dom'; +import mockAxios from 'jest-mock-axios'; +import userEvent from '@testing-library/user-event'; + +import SpellChecker from '../SpellChecker'; + +const input = 'hello'; + +const renderSpellChecker = (options?: MemoryHistoryBuildOptions): MemoryHistory => { + const history = createMemoryHistory(options); + + render( + + + , + ); + + return history; +}; + +const type = (input: string): HTMLDivElement => { + const divbox = screen.getByRole('textbox'); + divbox.innerText = input; + return divbox as HTMLDivElement; +}; + +const submit = () => userEvent.click(screen.getByRole('button')); + +it('allows selecting a language', () => { + renderSpellChecker(); + + const selector = screen.getByRole('combobox'); + userEvent.selectOptions(selector, screen.getByRole('option', { name: 'қазақша' })); + + expect((selector as HTMLSelectElement).value).toBe('kaz'); +}); + +it('allows typing an input', () => { + renderSpellChecker(); + + const textbox = type(input); + expect(textbox.innerText).toBe(input); +}); + +describe('URL state management', () => { + it('persists language and input', () => { + const history = renderSpellChecker({ initialEntries: [`/?q=${input}`] }); + expect(history.location.search).toBe(`?lang=kaz&q=${input}`); + }); + + it('discards invalid language', () => { + renderSpellChecker({ initialEntries: [`/?lang=kaza`] }); + + const selector = screen.getByRole('combobox'); + expect((selector as HTMLSelectElement).value).toBe('kaz'); + }); + + it('discards long input', () => { + const longInput = 'foobar'.repeat(500); + const history = renderSpellChecker({ initialEntries: [`/?lang=kaz&q=${longInput}`] }); + + expect(history.location.search).toBe(`?lang=kaz`); + }); +}); + +describe('analysis', () => { + it('no-ops an empty input', () => { + renderSpellChecker(); + submit(); + expect(mockAxios.post).not.toBeCalled(); + }); +}); From 33877f02d47ec32b4876dd5f1cfbcf582145ba69 Mon Sep 17 00:00:00 2001 From: satti-hari-krishna-reddy Date: Mon, 29 Jul 2024 23:04:22 +0530 Subject: [PATCH 24/26] reverting to spellers --- build.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.ts b/build.ts index 87159d26e..45d244594 100755 --- a/build.ts +++ b/build.ts @@ -72,7 +72,7 @@ const Plugin = { apyGet('list', {}), apyGet('list', { q: 'analyzers' }), apyGet('list', { q: 'generators' }), - apyGet('list', { q: 'spellcheckers' }), + apyGet('list', { q: 'spellers' }), ]); } catch (error) { let message = new String(error).toString(); From 5269f1fab86b3daa08a1a9152c0e53a43b7a42da Mon Sep 17 00:00:00 2001 From: satti-hari-krishna-reddy Date: Fri, 2 Aug 2024 00:11:43 +0530 Subject: [PATCH 25/26] fix small circle issue when no suggestions are returned --- src/components/spellchecker/SpellChecker.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/spellchecker/SpellChecker.tsx b/src/components/spellchecker/SpellChecker.tsx index 196639d7e..314982e0d 100644 --- a/src/components/spellchecker/SpellChecker.tsx +++ b/src/components/spellchecker/SpellChecker.tsx @@ -85,11 +85,15 @@ const SpellCheckForm = ({ setSuggestions(data); renderHighlightedText(text, data); setError(null); + setSelectedWord(null); + setSuggestionPosition(null); spellcheckResult.current = null; setLoading(false); } catch (error) { if (!axios.isCancel(error)) { setSuggestions([]); + setSelectedWord(null); + setSuggestionPosition(null); setError(error as Error); setLoading(false); } @@ -274,19 +278,19 @@ const SpellCheckForm = ({ - {selectedWord && suggestionPosition && ( + {selectedWord && suggestionPosition && suggestions.some((s) => s.token === selectedWord && s.sugg.length > 0) && (
{suggestions .find((s) => s.token === selectedWord) - ?.sugg.map((suggestion, index) => ( + ?.sugg?.map((suggestion, index) => (
applySuggestion(suggestion)} - onKeyDown={(event) => { + onKeyDown={(event: React.KeyboardEvent) => { if (event.key === 'Enter') { applySuggestion(suggestion); } From 34a4554bb557bd293665c809534753c9eecb97cc Mon Sep 17 00:00:00 2001 From: satti-hari-krishna-reddy Date: Sun, 4 Aug 2024 21:34:51 +0530 Subject: [PATCH 26/26] Added logic for instant spell check triggered after a delay of 3 seconds from the last keystroke. --- src/components/spellchecker/SpellChecker.tsx | 25 ++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/components/spellchecker/SpellChecker.tsx b/src/components/spellchecker/SpellChecker.tsx index 314982e0d..039d3d8b2 100644 --- a/src/components/spellchecker/SpellChecker.tsx +++ b/src/components/spellchecker/SpellChecker.tsx @@ -27,6 +27,9 @@ const Spellers: Readonly> = (window as any).SPELLERS; const langUrlParam = 'lang'; const textUrlParam = 'q'; + +const isKeyUpEvent = (event: React.SyntheticEvent): event is React.KeyboardEvent => event.type === 'keyup'; + const SpellCheckForm = ({ setLoading, setError, @@ -45,6 +48,10 @@ const SpellCheckForm = ({ const initialRender = React.useRef(true); const spellcheckRef = React.useRef(null); const spellcheckResult = React.useRef(null); + const spellCheckTimer = React.useRef(null); + + const instantSpellCheck = true; + const instantSpellCheckDelay = 3000; const [lang, setLang] = useLocalStorage('spellerLang', Object.keys(Spellers)[0], { overrideValue: toAlpha3Code(getUrlParam(history.location.search, langUrlParam)), @@ -101,6 +108,23 @@ const SpellCheckForm = ({ })(); }; + const handleInstantSpellCheck = ( + event: React.KeyboardEvent | React.ClipboardEvent, + ) => { + if (isKeyUpEvent(event) && (event.code === 'Space' || event.code === 'Enter')) { + return; + } + + if (spellCheckTimer.current && instantSpellCheck) { + clearTimeout(spellCheckTimer.current); + } + spellCheckTimer.current = window.setTimeout(() => { + if (spellCheckTimer) { + handleSubmit(); + } + }, instantSpellCheckDelay); + }; + const handleWordClick = React.useCallback((word: string, event: MouseEvent | TouchEvent) => { setSelectedWord(word); const rect = (event.currentTarget as Element).getBoundingClientRect(); @@ -265,6 +289,7 @@ const SpellCheckForm = ({ handleSubmit(); } }} + onKeyUp={handleInstantSpellCheck} ref={spellcheckRef} role="textbox" tabIndex={0}