WYSIWYGã¨ãã£ã¿ã«æ§ãã¦ContentEditableãããã£ã¦ã¿ãã¨ããã£ãããªåºç¡ãæ²¼ã ã¨ãæ°ã¥ããã¨ãã«ã¯ã©ã£ã·ã浸ãã£ã¦ããã¤ã ã°ã°ãã°ãããªãã«ç¥è¦ãè¦ã¤ããããè¯ãæãã®ã©ã¤ãã©ãªãããããã©åãããããã£ããã¨ã¯ãèªåã§ã³ã¼ããæ¸ãã¦å®ç¾ããã®ãæ©ãããããªãããªã¼ã¨æã£ã¦ãæ²¼ã«ãã¤ãããã
ãããããã¨ã¯ãã¯ã¼ããã½ããã¿ãããªãã¤ã¨ããããããç·¨éãã¦ããããã¹ãã®ã¹ã¿ã¤ã«ããªã¢ã«ã¿ã¤ã ã«å¤åããMarkdownã¨ãã£ã¿ã£ã½ããã¤ã ã¤ã¾ãã¦ã¼ã¶ãã¹ã¿ã¤ã«ãå½ã¦ãããããªãã¦ãã¦ã¼ã¶ãå ¥åããããã¹ãã«å¿ãã¦ã¹ã¿ã¤ã«ãå½ã¦ããã ããæ£ç¢ºã«ã¯ãããã¹ãã®è¦ãç®ãããããã«ããã ãã§ã¯ãªãã¦ããã£ã¨é«åº¦ãªä½ããReactã®ã³ã³ãã¼ãã³ãã¨ãããåãè¾¼ãã¨ããããã£ãã
Draft.jsãEditor.jsã®ãããªãContentEditableãæ±ãããããã¦ãããã©ã¤ãã©ãªãæ¤è¨ããã
Reactã®ã³ã³ãã¼ãã³ããåãè¾¼ãã®ãç°¡åããã ã£ãã
ãã ãContentEditableã®å
容ãå
¥åã®ãã³ã«åæ§ç¯ãããããªæ¹æ³ãæ¡ãã¨ããã£ã¬ããï¼ã¨ãã£ã¿å
ã«ããããã¹ããæ¿å
¥ãããç®æã示ã |
ããï¼ãç·¨éç®æã¨ã¯ç¡é¢ä¿ã«å
é ã¨ãã«é£ãã§ãã¾ãç¾è±¡ããã£ãã
ããã©ã¼ãã³ã¹ä¸ã®æ¸å¿µã¯ããã¤ã¤ããContentEditableã®å
容ãåæ§ç¯ããæ¹æ³ã¯ãã¦ã¼ã¶ã®å
¥åå
容ã解æãã¦UIã«åæ ããä¸ã§ä¸çªæ¥½ãªæ¹æ³ã ã¨æã£ãã®ã§ããããè²ããããªãã£ãã
ãããã¦ããã¨ããããã®ã«ç´ ã®ContentEditableãããããã¨ã¨ãªã£ãã
çµè«ã¨ãã¦ã¯ããã£ã¬ãããå¶ãè ã¯ContentEditableãå¶ããã ã åºåãããUI次第ã ãã©ãã£ã¬ãããè¯ãæãã«æ±ããã°ããã¨ã¯äºç´°ãªåé¡ã§ã¡ãã£ã¨ãããã¯ããã¯ã§ã©ãã«ã§ããªãã éã«åæããã¨ãããªæãã
- ContentEditableã«ããããã£ã¬ããã®ä½ç½®ãç¥ãããã«ã¯
document.getSelection()
ãªã©ã§Selectionãå¾ã¦ããã®focusNode
,focusOffset
ãªã©ã使ãã - ä¾ãã°ContentEditableãªè¦ç´ ãæã¤æåã®åè¦ç´ ãã
focusNode
ãè¦ã¤ããã¾ã§ãæåæ°ããã«ã¦ã³ããã¦ãããfocusOffset
ã足ãããå¼ããããã¦èª¿æ´ããã°ãããããã£ã¬ããã®ä½ç½®ã¨ãªãã - ãæåæ°ãã¯ãä½ã1æåã¨ãããããèªåã®ãã¸ãã¯ã®ä¸ã«è¨è¿°ãã¦ãããã¨ã«ãªããã©ãããã¯ã©ãããUIï¼DOMï¼ã«ãããã次第ï¼ãããç¸å½ããã©ãã®ã§ãï¼ã
- ContentEditableã§ãã£ã¬ãããæå®ã®ä½ç½®ã«ç§»åãããã«ã¯ãåãããã«Selectionã使ãã
selection.addRange(range)
ã§ãæå®ã®ä½ç½®ã«è¨å®ããRangeã渡ãã°OK - Rangeã«ã¯ãã©ã®è¦ç´ ãã®ãä½æåç®ãããæå®ããå¿ è¦ããã£ã¦ããããå°åºããããã«ãå ã»ã©ã®ãã£ã¬ããã®ä½ç½®ï¼ã«ã¦ã³ãããæåæ°ãå©ç¨ããã
- ContentEditableãªè¦ç´ ãæã¤æåã®åè¦ç´ ããé ã«èµ°æ»ãã¦ãæå®æåæ°ã«éããã¨ãã®è¦ç´ ããRangeã«è¨å®ãã¹ãè¦ç´ ã¨ãªãï¼ããè¦ä»¶æ¬¡ç¬¬ã§å ¨ç¶å¤ããï¼
以ä¸ãè足ã
åç´ãªã»ããã¢ããã§ã¯ãæå¾ ã©ããã«æåããªã
次ã®ã³ã¼ãã¯ãContentEditableãå®é¨ããReactã³ã³ãã¼ãã³ãã¨ãã¦ã¯å®å ¨ãªã³ã¼ãã ãã®å®è¡çµæãä¸ã®æ ã®ä¸ã§ç¢ºèªã§ããã®ã§ãä½ãããã¹ããå ¥åãã¦ãããããã
ã»ã¯ã·ã§ã³è¦åºãã«ãæ¸ãã¦ãããã©ãæå¾ ã©ããã«æåããªãã é£ç¶ãã¦ãã¼ãå©ãã¦åèªãå ¥åãããã¨ãã¦ãããã£ã¬ãããå é ããåãããhelloãã¨å ¥åããã¤ããããã®éé ãollehãã«ãªã£ã¦ãã¾ãã
onInput
ã§å
¥åã¤ãã³ããåã㦠innerHTML
ãuseState
ã®html
ã¨ãã¦ä¿åããã次ã®æç»æã«ä¿åããã html
ãContentEditableãªè¦ç´ ã«ã»ãããããã¨ããåç´ãªå¯¸æ³ã§ã¯ãã¡ã£ã½ãã
å ¥åæã«ãã£ã¬ããä½ç½®ãè¨æ¶ããç»é¢åæ å¾ã«ãã£ã¬ããä½ç½®ã復å ãã
éã«åæãããã£ã¬ããã®æ±ãæ¹ã®ã¨ãããå ¥åæã«ãã£ã¬ããä½ç½®ãè¨æ¶ããç»é¢åæ å¾ã«ãã£ã¬ããä½ç½®ã復å ããã°ãé£ç¶ãã¦ãã¼ãå©ãã¦ç®çã®åèªãå ¥åãããã¨ãå¯è½ã«ãªãã
ä¸è¨ã¯å®å
¨ãªã³ã¼ããªã®ã§ãä¾ç¤ºããã³ã¼ãã¨ãã¦ã¯ãã¤ã¸ã¼ã ãã©ç´°ãããã¸ãã¯ã¯ç¡è¦ãã¦ãã¾ããªãã
ãã£ã¬ããä½ç½®ç¹å®ã¨ãæå®ä½ç½®ã¸ã®ãã£ã¬ãã移åã«é¢ãããã³ãã«ã¯ãªããããã©ãæ£ã
è¨ã£ã¦ããã¨ããè¦ä»¶æ¬¡ç¬¬ã§ãã¸ãã¯ãå¤ããã®ã§ãã¾ãã£ã¨åèã«ã§ãããã®ã§ã¯ãªãã
ããã§ã¯ç¾å¨ã®ãã£ã¬ããä½ç½®ãåå¾ããé¢æ°ãgetCaretPosition
ãæå®ã®ä½ç½®ã¸ãã£ã¬ããã移åããé¢æ°ãmoveCaret
ã¨ããååã«ãã¦ããã
ãããã®å®ç¾©ãçãã¦ã³ã³ãã¼ãã³ãã ãã«ããã®ãã次ã®ã³ã¼ãã
export const Example2: FC = () => { const editorRef = useRef<HTMLDivElement>(null); const [html, setHtml] = useState(""); const [caretPosition, setCaretPosition] = useState(0); useEffect(() => { const editor = editorRef.current; if (editor == null) return; moveCaret(editor, caretPosition); }, [html]); const onInput = (e: FormEvent<HTMLDivElement>) => { const currentHtml = e.currentTarget.innerHTML; setHtml(currentHtml); setCaretPosition(getCaretPosition(e.currentTarget)); }; return ( <div> <div ref={editorRef} contentEditable onInput={onInput} dangerouslySetInnerHTML={{ __html: html }} /> </div> ); };
onInput
ã§å
¥åå
容ãä¿åããã¨ã¨ãã«ãgetCaretPosition
ãå¼ã³åºãã¦ãã®å¤ãä¿åããã
å
¥åå
容ï¼html
ï¼ã«åå¿ããuseEffect
ã®ä¸ã§moveCaret
ãå¼ã³åºãã¦ãå
ã»ã©ä¿åãããã£ã¬ããä½ç½®ã¨ContentEditableè¦ç´ ï¼editor
ï¼ãå¼æ°ã¨ãã¦ããã
ãhelloãã¨æã¦ã°ãhelloãã¨å ¥åãããã®ã§ä¸æ¦æåã ãã©ãå®ã¯æ¥æ¬èªã®å ¥åããããããªãã ãããã«ã¡ã¯ãã¨å ¥åãããã¨ããã¨ãkãnnnãtãhããã¨å ¥åããã¦ãã¾ãã
æ¥æ¬èªå ¥åã«å¯¾å¿ãã
onInput
ã«ããå
¥åãããã°ããããã®å
容ãæç»ãããã®ã§ããã¼ãå
¥åãæ¼¢åå¤æã®æãä¸ãã¦ãããªãã®ãåå ã£ã½ãã
ã¨ãããã¨ã§ãå
¥åã確å®ãããã¾ã§ç»é¢ã«åæ ããªãããã«ããã®ãã次ã®ã³ã¼ãã
è±åã§ãæ¥æ¬èªã§ãã«ã¦ã³ããããæåæ°ãè¦ç´ ã«å¤åãããããã§ã¯ãªãã®ã§getCaretPosition
ã¨moveCaret
ã¯ãã®ã¾ã¾ã
å¤æ´ç¹ã¯inputHtml
ã¨isInputting
ã¨ããç¶æ
ã追å ãããã¨ã
inputHtml
ã¯å
¥åãããå
容ããããã¡ã¼ãããhtml
ã«å
容ãæ¸ãè¾¼ãã¾ã§ã¯ç»é¢ã«åæ ãããªãã
isInputting
ã¯å
¥åä¸ãã¤ã¾ãå
¥åã確å®ããã¦ããªãã¨ãã«true
ã¨ãªããã©ã°ã
useEffect(() => { if (isInputting || intputHtml == null) return; setHtml(intputHtml); setInputHtml(undefined); }, [isInputting, intputHtml]); const onInput = (e: FormEvent<HTMLDivElement>) => { const currentHtml = e.currentTarget.innerHTML; setInputHtml(currentHtml); setCaretPosition(getCaretPosition(e.currentTarget)); };
ãã®ããã«onInput
ã§å
¥åãããå
容ãä¸æ¦inputHtml
ã«éé¿ããã¦ãå
¥åä¸ã§ãªãã¿ã¤ãã³ã°ã§inputHtml
ã®å
容ãhtml
ã«æ¸ãè¾¼ãããã®å¾ãinputHtml
ã«ã¯undefined
ã§ãå
¥ãã¦ããã¦ãããã¡ã¼ããã©ãã·ã¥ããã
å
¥åç¶æ
ã«ã¤ãã¦ã¯ onCompositionStart
㨠onCompositionEnd
ã§ã¤ãã³ããæ¾ãã
ãã¼ãåå
¥åãæ¼¢åå¤æãªã©ãå§ã¾ã£ãã¿ã¤ãã³ã°ã§åè
ãããããçµãã£ãã¿ã¤ãã³ã°ã§å¾è
ãå¼ã³åºãããã
<div ref={editorRef} contentEditable onInput={onInput} dangerouslySetInnerHTML={{ __html: html }} onCompositionStart={setInputting.bind(null, true)} onCompositionEnd={setInputting.bind(null, false)} />
Reactã³ã³ãã¼ãã³ããåãè¾¼ã
ããã¾ã§ã§ãããããã¨ã¯ContentEditableã«ä½ãå«ããããã©ããã£ã¬ãããè¨ç®ããããã¨ãã話ã«å°½ããã
ã¡ãã£ã¨ã¬ãã¬ãã ãã©ãå
¥åå
容ã«å¿ãã¦Reactã³ã³ãã¼ãã³ããContentEditableã«åãè¾¼ããã¤ã次ã®ã³ã¼ãã
[hoge]
ã¿ããã«å
¥åããã¨ãã©ãã«ã£ã½ããªãã
[hoge]
ãªã©ãã«ã¯ãããã¹ãã¨ãã¦ã¯6æåã ãã©ãç·¨éä¸å¯è½ï¼contenteditable="false"
ï¼ã«ãã¦ããã1æåã¨ãã¦ã«ã¦ã³ããããã
ãªã®ã§ getCaretPosition
ã¨moveCaret
ã®ãã¸ãã¯ãå¤æ´ããå¿
è¦ãããã
ä¾ãã°ãã£ã¬ããä½ç½®ãè¨ç®ããã¨ãã«è¦ç´ ãå帰çã«èµ°æ»ãããã©ãcontenteditable="false"
ã®ãããªç¹å®ã®è¦ç´ ãè¦ã¤ããæã«1æåã«ã¦ã³ããã¦ããã®åè¦ç´ ã¯ã¹ããããããªã©ã®ãã¸ãã¯ã追å ããã
Reactã³ã³ãã¼ãã³ããåãè¾¼ãããã«ã¯ãã¾ããã¬ã¼ã¹ãã«ãã¼ã¨ãããããã®åãè¾¼ãã¹ãå ´æã示ãè¦ç´ ãã¶ã¡ããã
æç²ããä¸ã®ã³ã¼ãã¯ãå
¥åãããå
容ãæç»ç¨HTMLã«å¤æããé¢æ°ã§<span contenteditable="false" class="label" data-text="${block.text}"></span>
ããReactã³ã³ãã¼ãã³ããåãè¾¼ãããã®è¦ç´ ã¨ãªãã
ã³ã³ãã¼ãã³ãã«propsã¨ãã¦æ¸¡ãããã®ãã¼ã¿ãè¦ç´ ã®å±æ§ã«è¼ãã¦ãããªã©ã®å·¥å¤«ããã¦ããã
const toHtml = (text: string): string => { const blocks = parse(text); return blocks .map((block) => { if (block.type === "normal") { return `<span>${block.text}</span>`; } return `<span contenteditable="false" class="label" data-text="${block.text}"></span>`; }) .join(""); };
æç»ç¨HTMLã®å¤æ´ãæ¤ç¥ãã¦ãããªãã¡useEffect
ã®ä¸ã§æ¬¡ã®ã³ã¼ããå®è¡ãããã¨ã§ãå
ã»ã©ã®ãã¬ã¼ã¹ãã«ãã¼ã«Reactã³ã³ãã¼ãã³ããåãè¾¼ã¾ããã
document.querySelectorAll(".label").forEach((label) => { const text = label.getAttribute("data-text"); render(<Label>{text}</Label>, label); });
ã¾ã¨ã