筋肉チョットデキル

コードが読みやすくなった!同単語をハイライトする実装

2020年06月24日

技術ブログReact

つくったもの

hover したらコードブロック内の同じ単語がハイライトされます!

word highlight

ブログのサンプルコードなんかを読んでて、

🤔

あれ?この変数ってどこで定義されてるんだ?

ってことがよくあり、ハイライトされるとコードが追いやすくなって便利かなと思って作りました!
Qiita からヒントを得て!(パクリ w)

実装

シンタックスハイライト

まず、シンタックスハイライトをするために当ブログではprism-react-rendererというライブラリを使っています。

prism-react-renderer の詳細は割愛しますが、
Highlightコンポーネントのcodeという state に表示するコード文字列を渡し、tokensという配列を操作しています。二次元の配列となっており、各行の中に各単語が入っています。
各単語それぞれに色付けをする必要があるので、各単語を<span>タグで囲って style を適用しています。

prism-react-rendererのサンプルコード
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) => (
<span
onMouseEnter={() => 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) => (
<span
onMouseEnter={() => 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 (
<span
onMouseEnter={() => 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) => (
<Word
key={key}
focusedWord={focusedWord}
setFocusedWord={setFocusedWord}
content={token.content.trim()}
tokenProps={getTokenProps({ token, key })}
/>
))}
</div>
))}
</pre>
)}
</Highlight>
);
};

時間差で背景色をつける

ホバーしてすぐ背景色がついてしまうと、たまたまマウスが文字の上を通っただけでも目まぐるしく色が変わってしまいます。

no timeout

そこで一定時間ホバーしたときに背景色が変わるようにしたいと思います。

/** @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 (
<span
onMouseEnter={() => 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) => (
<Word
key={key}
focusedWord={focusedWord}
setFocusedWord={setFocusedWord}
content={token.content.trim()}
tokenProps={getTokenProps({ token, key })}
/>
))}
</div>
))}
</pre>
)}
</Highlight>
);
};

これで無事、0.5 秒ホバーし続けたときだけ背景色が変わるように出来ました!

set timeout

🎉

難しくない実装ですが利便性が上がったのでステキ!


岡本 侑貴@筋肉チョットデキル

筋肉バカ。筋トレしてコード書いて、毎日幸せに生きてる。ボディビルにドハマリ。2021年東京オープンボディビル選手権で最高の身体に仕上げてつよつよエンジニアになる!!

Made with Gatsby& ChakraUi

Yuuki Okamoto • 2020