Created
September 22, 2025 04:11
-
-
Save HallexCosta/524ffee47991df115fa8e06ccc2c443c to your computer and use it in GitHub Desktop.
ritchtext
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React, { useRef, useEffect, useState, useMemo, useLayoutEffect } from 'react'; | |
| export function App() { | |
| const editorRef = useRef(null); | |
| const placeholderRef = useRef(null) | |
| const [text, setText] = useState('Lorem {sender}'); | |
| const [isEmpty, setIsEmpty] = useState(true) | |
| //const isEmpty = text === ''; | |
| // pega posição absoluta do cursor | |
| function getCaretCharacterOffsetWithin(element) { | |
| let caretOffset = 0; | |
| const sel = window.getSelection(); | |
| if (sel.rangeCount > 0) { | |
| const range = sel.getRangeAt(0); | |
| const preCaretRange = range.cloneRange(); | |
| preCaretRange.selectNodeContents(element); | |
| preCaretRange.setEnd(range.endContainer, range.endOffset); | |
| caretOffset = preCaretRange.toString().length; | |
| } | |
| return caretOffset; | |
| } | |
| // restaura o cursor na posição salva | |
| function setCaretPosition(element, offset) { | |
| let nodeStack = [element], | |
| node; | |
| let charIndex = 0; | |
| let range = document.createRange(); | |
| range.setStart(element, 0); | |
| range.collapse(true); | |
| while ((node = nodeStack.pop())) { | |
| if (node.nodeType === 3) { | |
| // text node | |
| let nextCharIndex = charIndex + node.length; | |
| if (offset >= charIndex && offset <= nextCharIndex) { | |
| range.setStart(node, offset - charIndex); | |
| range.collapse(true); | |
| break; | |
| } | |
| charIndex = nextCharIndex; | |
| } else { | |
| let i = node.childNodes.length; | |
| while (i--) { | |
| nodeStack.push(node.childNodes[i]); | |
| } | |
| } | |
| } | |
| let sel = window.getSelection(); | |
| sel.removeAllRanges(); | |
| sel.addRange(range); | |
| } | |
| function extractExtraText(text, placeholder) { | |
| // transforma placeholder em regex literal | |
| const regexStr = placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); | |
| // remove o placeholder literal do texto | |
| const result = text.replace(new RegExp(regexStr, "g"), "").trim(); | |
| return result; | |
| } | |
| function highlightContent(el) { | |
| const caretPos = getCaretCharacterOffsetWithin(el); | |
| const text = el.textContent; | |
| const isEquals = text === placeholderRef.current?.innerText | |
| const extraText = extractExtraText(text, placeholderRef.current?.innerText) | |
| console.log({ | |
| editorRef: text, | |
| placeholderRef: placeholderRef.current?.innerText, | |
| extractExtraText: extraText, | |
| extractTextV2: text.slice(1, placeholderRef.current?.innerText.length - 1), | |
| isEquals, | |
| }) | |
| // if (isEmpty && !isEquals) { | |
| // setIsEmpty(false) | |
| // } | |
| if (extraText.length > 0) { | |
| setIsEmpty(false) | |
| } | |
| setText(extraText) | |
| const tokens = text.split(/(\s+)/); | |
| let newHTML = ''; | |
| tokens.forEach(token => { | |
| if (/^\{[^}]+\}$/.test(token)) { | |
| newHTML += `<span style="color:orange;font-weight:bold">${token}</span>`; | |
| } else { | |
| newHTML += token; | |
| } | |
| }); | |
| el.innerHTML = newHTML; | |
| setCaretPosition(el, caretPos); | |
| } | |
| useEffect(() => { | |
| const el = editorRef.current; | |
| highlightContent(el); | |
| }, []); | |
| function handleShowValue() { | |
| if (editorRef.current) { | |
| const plainText = editorRef.current.textContent; // só texto | |
| const htmlValue = editorRef.current.innerHTML; // com spans coloridos | |
| console.log('Texto puro:', plainText); | |
| console.log('Com highlight:', htmlValue); | |
| setText(plainText); | |
| // console.log('text', text) | |
| } | |
| } | |
| useEffect(() => { | |
| const handleSelectionChange = () => { | |
| const sel = window.getSelection(); | |
| if (!sel || !editorRef.current) return; | |
| if (editorRef.current.contains(sel.anchorNode)) { | |
| // caret position relative to text content | |
| // setCaretPos(sel.anchorOffset); | |
| if (isEmpty) { | |
| // moveCaretToStart(editorRef.current) | |
| } | |
| console.log({ | |
| text | |
| }) | |
| console.log('sel.anchor', sel.anchorOffset, isEmpty) | |
| } | |
| }; | |
| document.addEventListener("selectionchange", handleSelectionChange); | |
| return () => { | |
| document.removeEventListener("selectionchange", handleSelectionChange); | |
| }; | |
| }, []); | |
| const moveCaretToStart = (el) => { | |
| if (!el) return; | |
| setTimeout(() => { | |
| const selection = window.getSelection(); | |
| const range = document.createRange(); | |
| // Ensure we target the *contentEditable* element | |
| range.setStart(el, 0); | |
| range.collapse(true); | |
| selection.removeAllRanges(); | |
| selection.addRange(range); | |
| }, 1) | |
| }; | |
| // useEffect(() => { | |
| // if (text.length) { | |
| // setIsEmpty(false) | |
| // } else { | |
| // setIsEmpty(true) | |
| // } | |
| // }, [text]) | |
| const handleOnFocus = (e) => { | |
| // console.log({isEmpty}) | |
| if (isEmpty) { | |
| moveCaretToStart(editorRef.current); | |
| } | |
| }; | |
| console.log({ | |
| isEmpty | |
| }) | |
| return ( | |
| <> | |
| <div | |
| ref={editorRef} | |
| contentEditable | |
| suppressContentEditableWarning | |
| onInput={e => highlightContent(e.currentTarget)} | |
| onFocus={handleOnFocus} | |
| style={{ | |
| width: '400px', | |
| minHeight: '120px', | |
| padding: '10px', | |
| backgroundColor: '#222', | |
| color: 'white', | |
| fontSize: '14px', | |
| fontFamily: 'monospace', | |
| whiteSpace: 'pre-wrap', | |
| outline: 'none', | |
| position: 'relative', | |
| opacity: isEmpty ? 0.5 : 1, | |
| }} | |
| > | |
| <div | |
| ref={placeholderRef} | |
| style={{ | |
| position: 'absolute', | |
| top: '10px', | |
| left: '10px', | |
| color: '#888', | |
| pointerEvents: 'none', | |
| fontFamily: 'monospace', | |
| fontSize: '14px', | |
| display: isEmpty ? 'block' : 'none' | |
| }} | |
| > | |
| {`Olá! Eu sou {sender_name} e escrevo para demonstrar interesse na posição de {job_title} na {company}. Podem me contatar via WhatsApp: {whatsapp_link} ou LinkedIn: {linkedin_link}`} | |
| </div> | |
| {text} | |
| </div> | |
| <button onClick={handleShowValue}>Mostrar valor</button> | |
| </> | |
| ); | |
| } | |
| export default App |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Now it works
Only placeholder:

With Text:

Code working: