@ai4bharat/indic-transliterate
Version:
Transliterate component for React
1 lines • 16.3 kB
Source Map (JSON)
{"mappings":"AGAA,uBAAuB,MAAM,CAAC;ACE9B,wCACE,SAAQ,KAAK,CAAC,SAAS,CAAC,gBAAgB,GAAG,mBAAmB,CAAC;IAC/D;;;;OAIG;IACH,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,MAAM,SAAS,CAAC;IAElD;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAE5B;;OAEG;IACH,eAAe,CAAC,EAAE,MAAM,aAAa,CAAC;IAEtC;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,aAAa,CAAC;IAEvC;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,IAAI,CAAC,EAAE,QAAQ,CAAC;IAEhB;;OAEG;IACH,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAErC;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;;OAIG;IACH,gCAAgC,CAAC,EAAE,OAAO,CAAC;IAE3C;;;;OAIG;IACH,2BAA2B,CAAC,EAAE,MAAM,CAAC;IAErC;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IAEvB;;;OAGG;IACH,4BAA4B,CAAC,EAAE,OAAO,CAAC;IAEvC;;;OAGG;IACH,+BAA+B,CAAC,EAAE,OAAO,CAAC;IAE1C;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;ACtGD,kBAAyB;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,KAAK,GAAG,KAAK,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,QAAQ,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,QAAQ,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;ACZF,OAAO,MAAM;;;;;CAKZ,CAAC;ACHF,cAAc;IAEZ,+BAA+B,CAAC,EAAE,OAAO,CAAC;IAC1C,IAAI,CAAC,EAAE,QAAQ,CAAC;CACjB,CAAC;AAqCF,OAAO,MAAM,oCACL,MAAM,gBACE,MAAM,UACZ,MAAM,kCAEb,OAAO,CAAC,MAAM,EAAE,GAAG,SAAS,CA8E9B,CAAC;AE3HF,OAAO,MAAM,mCAAwC,OAAO,CAC1D,UAAU,EAAE,GAAG,SAAS,CA2BzB,CAAC;ACVF,OAAO,MAAM,0XA6BV,uBAAuB,KAAG,IAAI,OA2XhC,CAAC","sources":["src/src/util/touch-util.ts","src/src/util/caret-util.ts","src/src/util/index.ts","src/src/types/Language.ts","src/src/interfaces/Props.ts","src/src/types/LangObject.ts","src/src/constants/TriggerKeys.ts","src/src/util/suggestions-util.ts","src/src/constants/Urls.ts","src/src/util/getTransliterationLanguages.ts","src/src/index.tsx","src/index.tsx"],"sourcesContent":[null,null,null,null,null,null,null,null,null,null,null,"import * as React from \"react\";\nimport { useEffect, useRef, useState, useMemo } from \"react\";\nimport { setCaretPosition, getInputSelection, isTouchEnabled } from \"./util\";\nimport getCaretCoordinates from \"textarea-caret\";\nimport { IndicTransliterateProps } from \"./interfaces/Props\";\nimport { Language } from \"./types/Language\";\nimport { LangObject } from \"./types/LangObject\";\nimport { TriggerKeys } from \"./constants/TriggerKeys\";\nimport { getTransliterateSuggestions } from \"./util/suggestions-util\";\nimport { getTransliterationLanguages } from \"./util/getTransliterationLanguages\";\nimport { BASE_URL_TL } from \"./constants/Urls\";\n\nconst KEY_UP = \"ArrowUp\";\nconst KEY_DOWN = \"ArrowDown\";\nconst KEY_LEFT = \"ArrowLeft\";\nconst KEY_RIGHT = \"ArrowRight\";\nconst KEY_ESCAPE = \"Escape\";\n\nconst OPTION_LIST_Y_OFFSET = 10;\nconst OPTION_LIST_MIN_WIDTH = 100;\n\nexport const IndicTransliterate = ({\n renderComponent = (props) => <input {...props} />,\n lang = \"hi\",\n offsetX = 0,\n offsetY = 10,\n onChange,\n onChangeText,\n onBlur,\n value,\n onKeyDown,\n containerClassName = \"\",\n containerStyles = {},\n activeItemStyles = {},\n maxOptions = 5,\n hideSuggestionBoxOnMobileDevices = false,\n hideSuggestionBoxBreakpoint = 450,\n triggerKeys = [\n TriggerKeys.KEY_SPACE,\n TriggerKeys.KEY_ENTER,\n TriggerKeys.KEY_RETURN,\n TriggerKeys.KEY_TAB,\n ],\n insertCurrentSelectionOnBlur = true,\n showCurrentWordAsLastSuggestion = true,\n enabled = true,\n horizontalView = false,\n customApiURL = BASE_URL_TL,\n apiKey = \"\",\n ...rest\n}: IndicTransliterateProps): JSX.Element => {\n interface LogJson {\n keystrokes: any;\n results: any;\n opted: any;\n created_at: any;\n language: any;\n }\n const [left, setLeft] = useState(0);\n const [top, setTop] = useState(0);\n const [selection, setSelection] = useState<number>(0);\n const [matchStart, setMatchStart] = useState(-1);\n const [matchEnd, setMatchEnd] = useState(-1);\n const inputRef = useRef<HTMLInputElement>(null);\n const [windowSize, setWindowSize] = useState({ width: 0, height: 0 });\n const [direction, setDirection] = useState(\"ltr\");\n const [googleFont, setGoogleFont] = useState<string | null>(null);\n const [options, setOptions] = useState<string[]>([]);\n const [logJsonArray, setLogJsonArray] = useState<LogJson[]>([]);\n const [numSpaces, setNumSpaces] = useState(0);\n const [parentUuid, setParentUuid] = useState(\"0\");\n const [uuid, setUuid] = useState(Math.random().toString(36).substr(2, 9));\n const [subStrLength, setSubStrLength] = useState(0);\n const [restart, setRestart] = useState(true);\n\n const shouldRenderSuggestions = useMemo(\n () =>\n hideSuggestionBoxOnMobileDevices\n ? windowSize.width > hideSuggestionBoxBreakpoint\n : true,\n [windowSize, hideSuggestionBoxBreakpoint, hideSuggestionBoxOnMobileDevices],\n );\n\n const reset = () => {\n // reset the component\n setSelection(0);\n setOptions([]);\n };\n\n const handleSelection = (index: number) => {\n const currentString = value;\n // create a new string with the currently typed word\n // replaced with the word in transliterated language\n const newValue =\n currentString.substring(0, matchStart) +\n options[index] +\n \" \" +\n currentString.substring(matchEnd + 1, currentString.length);\n\n if(logJsonArray.length){\n let lastLogJson = logJsonArray[logJsonArray.length-1];\n let logJson:LogJson = {\n keystrokes: lastLogJson.keystrokes,\n results: lastLogJson.results,\n opted: options[index],\n created_at: new Date().toISOString(),\n language: lang};\n setLogJsonArray([...logJsonArray, logJson]);\n setNumSpaces(numSpaces+1);\n }\n\n // set the position of the caret (cursor) one character after the\n // the position of the new word\n setTimeout(() => {\n setCaretPosition(\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n inputRef.current!,\n matchStart + options[index].length + 1\n );\n }, 1);\n\n // bubble up event to the parent component\n const e = {\n target: { value: newValue },\n } as unknown as React.FormEvent<HTMLInputElement>;\n onChangeText(newValue);\n onChange && onChange(e);\n reset();\n return inputRef.current?.focus();\n };\n\n const renderSuggestions = async (lastWord: string, wholeText: string) => {\n if (!shouldRenderSuggestions) {\n return;\n }\n // fetch suggestion from api\n // const url = `https://www.google.com/inputtools/request?ime=transliteration_en_${lang}&num=5&cp=0&cs=0&ie=utf-8&oe=utf-8&app=jsapi&text=${lastWord}`;\n\n // const numOptions = showCurrentWordAsLastSuggestion\n // ? maxOptions - 1\n // : maxOptions;\n\n const data = await getTransliterateSuggestions(lastWord, customApiURL, apiKey, {\n // numOptions,\n showCurrentWordAsLastSuggestion,\n lang,\n });\n setOptions(data ?? []);\n let logJson:LogJson = {\n keystrokes: wholeText,\n results: data,\n opted: \"\",\n created_at: new Date().toISOString(),\n language: lang}\n\n if(restart){\n setRestart(false);\n setLogJsonArray([logJson]);\n }else{\n setLogJsonArray([...logJsonArray, logJson]);\n }\n };\n\n const getDirectionAndFont = async (lang: Language) => {\n const langList = await getTransliterationLanguages();\n const langObj = langList?.find((l) => l.LangCode === lang) as LangObject;\n return [\n langObj?.Direction ?? \"ltr\",\n langObj?.GoogleFont,\n langObj?.FallbackFont,\n ];\n };\n\n const handleChange = (e: React.FormEvent<HTMLInputElement>) => {\n const value = e.currentTarget.value;\n\n if(numSpaces == 0 || restart){\n if(value.length >= 4){\n setSubStrLength(value.length-4);\n }else{\n setSubStrLength(0);\n }\n } \n\n if (numSpaces >= 5){\n const finalJson = {\"uuid\": uuid, \"parent_uuid\": parentUuid, \"word\": value, \"source\": localStorage.getItem('source') != undefined ? localStorage.getItem('source') : \"node-module\", \"language\": lang, \"steps\":logJsonArray};\n setLogJsonArray([]);\n setParentUuid(uuid);\n setUuid(Math.random().toString(36).substr(2, 9));\n setSubStrLength(value.length-2);\n setNumSpaces(0);\n setRestart(true);\n fetch(\"https://backend.shoonya.ai4bharat.org/logs/transliteration_selection/\", {\n method: \"POST\",\n body: JSON.stringify(finalJson),\n headers: {\n \"Content-Type\": \"application/json\"\n },\n })\n .then(async (res) => {\n if (!res.ok) {throw await res.json()};\n })\n .catch((err) => {\n console.log(\"error\", err);\n });\n }\n\n // bubble up event to the parent component\n onChange && onChange(e);\n onChangeText(value);\n\n if (!shouldRenderSuggestions) {\n return;\n }\n\n // get the current index of the cursor\n const caret = getInputSelection(e.target as HTMLInputElement).end;\n const input = inputRef.current;\n\n if (!input) return;\n\n const caretPos = getCaretCoordinates(input, caret);\n\n // search for the last occurence of the space character from\n // the cursor\n const indexOfLastSpace =\n value.lastIndexOf(\" \", caret - 1) < value.lastIndexOf(\"\\n\", caret - 1)\n ? value.lastIndexOf(\"\\n\", caret - 1)\n : value.lastIndexOf(\" \", caret - 1);\n\n // first character of the currently being typed word is\n // one character after the space character\n // index of last character is one before the current position\n // of the caret\n setMatchStart(indexOfLastSpace + 1);\n setMatchEnd(caret - 1);\n\n // currentWord is the word that is being typed\n const currentWord = value.slice(indexOfLastSpace + 1, caret);\n if (currentWord && enabled) {\n // make an api call to fetch suggestions\n if(numSpaces == 0 || restart){\n if(value.length >= 4){\n renderSuggestions(currentWord, value.substr(value.length-4, value.length));\n }else{\n renderSuggestions(currentWord, value.substr(0, value.length));\n }\n }else{\n renderSuggestions(currentWord, value.substr(subStrLength, value.length));\n }\n\n const rect = input.getBoundingClientRect();\n\n // calculate new left and top of the suggestion list\n\n // minimum of the caret position in the text input and the\n // width of the text input\n const left = Math.min(\n caretPos.left,\n rect.width - OPTION_LIST_MIN_WIDTH / 2,\n );\n\n // minimum of the caret position from the top of the input\n // and the height of the input\n const top = Math.min(caretPos.top + OPTION_LIST_Y_OFFSET, rect.height);\n\n setTop(top);\n setLeft(left);\n } else {\n reset();\n }\n };\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {\n const helperVisible = options.length > 0;\n\n if (helperVisible) {\n if (triggerKeys.includes(event.key)) {\n event.preventDefault();\n handleSelection(selection);\n } else {\n switch (event.key) {\n case KEY_ESCAPE:\n event.preventDefault();\n reset();\n break;\n case KEY_UP:\n event.preventDefault();\n setSelection((options.length + selection - 1) % options.length);\n break;\n case KEY_DOWN:\n event.preventDefault();\n setSelection((selection + 1) % options.length);\n break;\n case KEY_LEFT:\n event.preventDefault();\n setSelection((options.length + selection - 1) % options.length);\n break;\n case KEY_RIGHT:\n event.preventDefault();\n setSelection((selection + 1) % options.length);\n break;\n default:\n onKeyDown && onKeyDown(event);\n break;\n }\n }\n } else {\n onKeyDown && onKeyDown(event);\n }\n };\n\n const handleBlur = (\n event: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>,\n ) => {\n if (!isTouchEnabled()) {\n if (insertCurrentSelectionOnBlur && options[selection]) {\n handleSelection(selection);\n } else {\n reset();\n }\n }\n onBlur && onBlur(event);\n };\n\n const handleResize = () => {\n // TODO implement the resize function to resize\n // the helper on screen size change\n const width = window.innerWidth;\n const height = window.innerHeight;\n setWindowSize({ width, height });\n };\n\n useEffect(() => {\n window.addEventListener(\"resize\", handleResize);\n const width = window.innerWidth;\n const height = window.innerHeight;\n setWindowSize({ width, height });\n\n return () => {\n window.removeEventListener(\"resize\", handleResize);\n };\n }, []);\n\n useEffect(() => {\n getDirectionAndFont(lang).then(([direction, googleFont, fallbackFont]) => {\n setDirection(direction);\n // import google font if not already imported\n if (googleFont) {\n if (!document.getElementById(`font-${googleFont}`)) {\n const link = document.createElement(\"link\");\n link.id = `font-${googleFont}`;\n link.href = `https://fonts.googleapis.com/css?family=${googleFont}`;\n link.rel = \"stylesheet\";\n document.head.appendChild(link);\n }\n setGoogleFont(`${googleFont}, ${fallbackFont ?? \"sans-serif\"}`);\n } else {\n setGoogleFont(null);\n }\n });\n }, [lang]);\n\n return (\n <div\n // position relative is required to show the component\n // in the correct position\n style={{\n ...containerStyles,\n position: \"relative\",\n }}\n className={containerClassName}\n >\n {renderComponent({\n onChange: handleChange,\n onKeyDown: handleKeyDown,\n onBlur: handleBlur,\n ref: inputRef,\n value: value,\n \"data-testid\": \"rt-input-component\",\n lang: lang,\n style: {\n direction: direction,\n ...(googleFont && { fontFamily: googleFont }),\n },\n ...rest,\n })}\n {shouldRenderSuggestions && options.length > 0 && (\n <ul\n style={{\n backgroundClip : \"padding-box\",\n backgroundColor : \"#fff\",\n border : \"1px solid rgba(0, 0, 0, 0.15)\",\n boxShadow : \"0 6px 12px rgba(0, 0, 0, 0.175)\",\n display: horizontalView ? \"flex\" : \"block\",\n fontSize: \"14px\",\n listStyle: \"none\",\n padding: \"1px\",\n textAlign: \"center\",\n zIndex: 20000,\n left: `${left + offsetX}px`,\n top: `${top + offsetY}px`,\n position: \"absolute\",\n width: \"auto\",\n ...(googleFont && { fontFamily: googleFont }),\n }}\n data-testid=\"rt-suggestions-list\"\n lang={lang}\n >\n {/*\n * convert to set and back to prevent duplicate list items\n * that might happen while using backspace\n */}\n {Array.from(new Set(options)).map((item, index) => (\n <li\n style={index === selection ? { cursor: \"pointer\",padding: \"10px\",minWidth: \"100px\",backgroundColor: \"#65c3d7\", color:\"#fff\"} : { cursor: \"pointer\",padding: \"10px\",minWidth: \"100px\",backgroundColor: \"#fff\"} }\n onMouseEnter={() => {\n setSelection(index);\n }}\n onClick={() => handleSelection(index)}\n key={item}\n >\n {item}\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n};\n\nexport type { IndicTransliterateProps, Language };\nexport { TriggerKeys, getTransliterateSuggestions };\nexport { getTransliterationLanguages };\n"],"names":[],"version":3,"file":"types.d.ts.map","sourceRoot":"../"}