コードが読みやすくなった!同単語をハイライトする実装
2020年06月24日
つくったもの
hover したらコードブロック内の同じ単語がハイライトされます!
ブログのサンプルコードなんかを読んでて、
🤔
あれ?この変数ってどこで定義されてるんだ?
ってことがよくあり、ハイライトされるとコードが追いやすくなって便利かなと思って作りました!
Qiita からヒントを得て!(パクリ w)
実装
シンタックスハイライト
まず、シンタックスハイライトをするために当ブログではprism-react-rendererというライブラリを使っています。
prism-react-renderer の詳細は割愛しますが、
Highlight
コンポーネントのcode
という state に表示するコード文字列を渡し、tokens
という配列を操作しています。二次元の配列となっており、各行の中に各単語が入っています。
各単語それぞれに色付けをする必要があるので、各単語を<span>
タグで囲って style を適用しています。
import React from 'react';import Highlight, { defaultProps } from 'prism-react-renderer';const exampleCode = `(function someDemo() {var test = "Hello World!";console.log(test);})();return () => <App />;`.trim();export default () => (<Highlight {...defaultProps} code={exampleCode} language='jsx'>{({ className, style, tokens, getLineProps, getTokenProps }) => (<pre className={className} style={style}>{tokens.map((line, i) => (<div {...getLineProps({ line, key: i })}>{line.map((token, key) => (<span {...getTokenProps({ token, key })} />))}</div>))}</pre>)}</Highlight>);
ホバーした文字列を state で管理する
token.content
で単語の文字列を取得できるので、空白を除去して state 管理します。
import React, { useState } from 'react';import Highlight, { defaultProps } from 'prism-react-renderer';const exampleCode = `(function someDemo() {var test = "Hello World!";console.log(test);})();return () => <App />;`.trim();export default () => {const [focusedWord, setFocusedWord] = useState('');return (<Highlight {...defaultProps} code={exampleCode} language='jsx'>{({ className, style, tokens, getLineProps, getTokenProps }) => (<pre className={className} style={style}>{tokens.map((line, i) => (<div {...getLineProps({ line, key: i })}>{line.map((token, key) => (<spanonMouseEnter={() => setFocusedWord(token.content.trim())}onMouseLeave={() => setFocusedWord('')}{...getTokenProps({ token, key })}/>))}</div>))}</pre>)}</Highlight>);};
ホバーした文字と同じ文字の背景に色を付ける
state のfocusedWord
と同じ文字であり、1 文字より大きいものの背景に色をつけます。
また、CSS と同じような記述ができることから、このサンプルコードではemotionという CSS-in-JS のライブラリの css prop を使っています。
/** @jsx jsx */import React, { useState } from 'react';import Highlight, { defaultProps } from 'prism-react-renderer';import { css, jsx } from '@emotion/core';const exampleCode = `(function someDemo() {var test = "Hello World!";console.log(test);})();return () => <App />;`.trim();export default () => {const [focusedWord, setFocusedWord] = useState('');const shouldHighlighted = word => focusedWord === word && word.length > 1;return (<Highlight {...defaultProps} code={exampleCode} language='jsx'>{({ className, style, tokens, getLineProps, getTokenProps }) => (<pre className={className} style={style}>{tokens.map((line, i) => (<div {...getLineProps({ line, key: i })}>{line.map((token, key) => (<spanonMouseEnter={() => setFocusedWord(token.content.trim())}onMouseLeave={() => setFocusedWord('')}css={shouldHighlighted(token.content.trim()) &&css`background-color: rgba(254, 235, 200, 0.24);`}{...getTokenProps({ token, key })}/>))}</div>))}</pre>)}</Highlight>);};
Word コンポーネントに分ける
少しコードが増えてきたので 1 単語をレンダリングするコンポーネントを作りたいと思います!
/** @jsx jsx */import React, { useState } from 'react';import Highlight, { defaultProps } from 'prism-react-renderer';import { css, jsx } from '@emotion/core';const exampleCode = `(function someDemo() {var test = "Hello World!";console.log(test);})();return () => <App />;`.trim();const Word = ({ focusedWord, setFocusedWord, content, tokenProps }) => {const shouldHighlighted = word => focusedWord === word && word.length > 1;return (<spanonMouseEnter={() => setFocusedWord(content)}onMouseLeave={() => setFocusedWord('')}css={shouldHighlighted(content) &&css`background-color: rgba(254, 235, 200, 0.24);`}{...tokenProps}/>);};export default () => {const [focusedWord, setFocusedWord] = useState('');return (<Highlight {...defaultProps} code={exampleCode} language='jsx'>{({ className, style, tokens, getLineProps, getTokenProps }) => (<pre className={className} style={style}>{tokens.map((line, i) => (<div {...getLineProps({ line, key: i })}>{line.map((token, key) => (<Wordkey={key}focusedWord={focusedWord}setFocusedWord={setFocusedWord}content={token.content.trim()}tokenProps={getTokenProps({ token, key })}/>))}</div>))}</pre>)}</Highlight>);};
時間差で背景色をつける
ホバーしてすぐ背景色がついてしまうと、たまたまマウスが文字の上を通っただけでも目まぐるしく色が変わってしまいます。
そこで一定時間ホバーしたときに背景色が変わるようにしたいと思います。
/** @jsx jsx */import React, { useState } from 'react';import Highlight, { defaultProps } from 'prism-react-renderer';import { css, jsx } from '@emotion/core';const exampleCode = `(function someDemo() {var test = "Hello World!";console.log(test);})();return () => <App />;`.trim();const Word = ({ focusedWord, setFocusedWord, content, tokenProps }) => {const [delayHandler, setDelayHandler] = useState(null);const handleMouseEnter = word => {setDelayHandler(setTimeout(() => {setFocusedWord(word);}, 500));};const handleMouseLeave = () => {setFocusedWord('');clearTimeout(delayHandler);};const shouldHighlighted = word => focusedWord === word && word.length > 1;return (<spanonMouseEnter={() => handleMouseEnter(content)}onMouseLeave={handleMouseLeave}css={shouldHighlighted(content) &&css`background-color: rgba(254, 235, 200, 0.24);`}{...tokenProps}/>);};export default () => {const [focusedWord, setFocusedWord] = useState('');return (<Highlight {...defaultProps} code={exampleCode} language='jsx'>{({ className, style, tokens, getLineProps, getTokenProps }) => (<pre className={className} style={style}>{tokens.map((line, i) => (<div {...getLineProps({ line, key: i })}>{line.map((token, key) => (<Wordkey={key}focusedWord={focusedWord}setFocusedWord={setFocusedWord}content={token.content.trim()}tokenProps={getTokenProps({ token, key })}/>))}</div>))}</pre>)}</Highlight>);};
これで無事、0.5 秒ホバーし続けたときだけ背景色が変わるように出来ました!
🎉
難しくない実装ですが利便性が上がったのでステキ!