前置き
この記事は、大学2年の春休みにインプット力とアウトプット力を鍛えるべく毎日記事を投稿する試みの第2本目の記事です。
筆者は記事投稿経験がほぼ無く、プログラミング歴も2年ほどと浅いです。ですので間違い等を発見された場合は優しくご指摘ください。
前回
概要
先日Reactにて画像をクリックすると拡大表示される、いわゆるライトボックスの改善を行っていました。
その際にその時使っていたライブラリでは要件を満たすのが厳しかったので、新しく photoswipeというものを使うことにしました。
以前使っていたものは拡大表示後の画像をさらに拡大する際、ブラウザデフォルトの拡大縮小しか使えませんでした。無理矢理スクロールバーを追加することも考えましたが、Reactで直接DOM操作をする必要が出てきたのでそれを避けるため新しくライブラリを選定することにしました。
photoswipeにはデフォルトで拡大縮小が組み込まれていたからです。
しかしここで新たな問題が発生しました。それは拡大表示をする際に、画像をクリックするのではなく拡大アイコンを押して表示するという方式をとる必要があったためです。この要件を満たすために試行錯誤した経験をメモと言う形で残したいと思います。
試行錯誤の過程
初めてプロジェクト内でライブラリを選定したのもあって、このタスクを任されてから思っていたより時間が経っていたこともあり焦っていました。そのためはじめは深くコードを考えずに実装していました。
まずreactを使っていたのでそのままドキュメントの最後にあるforReactをコピペして実装していました。
ですが当然そのままでは画像をクリックすると画像が拡大されてしまいました。
また、ドキュメントのコードでは画像の一覧をSimpleGalleryに下記のように渡していました。
<div>
<SimpleGallery
galleryID="my-test-gallery"
images={[
{
largeURL:
'https://cdn.photoswipe.com/photoswipe-demo-images/photos/1/img-2500.jpg',
thumbnailURL:
'https://cdn.photoswipe.com/photoswipe-demo-images/photos/1/img-200.jpg',
width: 1875,
height: 2500,
},
{
largeURL:
'https://cdn.photoswipe.com/photoswipe-demo-images/photos/2/img-2500.jpg',
thumbnailURL:
'https://cdn.photoswipe.com/photoswipe-demo-images/photos/2/img-200.jpg',
width: 1669,
height: 2500,
},
{
largeURL:
'https://cdn.photoswipe.com/photoswipe-demo-images/photos/3/img-2500.jpg',
thumbnailURL:
'https://cdn.photoswipe.com/photoswipe-demo-images/photos/3/img-200.jpg',
width: 2500,
height: 1666,
},
]}
/>
</div>
しかし元のコードではurlの入ったデータからmapで取ってきていたので、一度に渡せず困りました。
どこまで載せていいかわからないので、長切り取ったコードだけ置いておきます。
( addFilterを使うと動的に追加できるようですが、この時は存在に気づいていないかつ結局全部追加してからlightboxに渡す必要がありそうだったので断念しました。)
{ dataImages[className]?.map((mappedImage, index) => (
<div key={index}
{mappedImage.image_url ? (
<div>
<>
<Image
src={mappedImage.image_url}
alt="Card"
width={500}
height={300}
/>
<div
onClick={(e) => {
e.stopPropagation()
handleExpandClick(index)
}}
>
<Image
src="/images/expand_image.svg"
alt="Expand"
/>
</div>
</>
) : (
<p
style={{
margin: "auto",
}}
>
No image available
</p>
)}
</div>
上記の handleExpandClick(index)で前のライブラリでは拡張するように実装していました。とりあえず何とか同じようにするため生成AIに投げてコードを修正しましたが、今では考えられないような実装でした。
まず、SimpleGallery.tsx(例はjs)からlightbox.init()を削除して,画像のonClickにe.preventDefault()でデフォルトのクリックイベントを無効化して拡大表示が発火しないようにしました。ちなみに最初に実装した時点でaタグからdivタグに変えてます。
ではどのようにして拡大を表示したかというと、まず先程のコードの画像の部分をSimpleGalleryに変更しました。
<Gallery
imageURL={mappedImage.image_url}
index={index}
galleryID={className}
/>
続いてhandleExpandClickを下記のように実装しました。
const handleExpandClick = (index: number, imageUrl: string | null) => {
const image = [{ src: imageUrl || "", w: 1200, h: 900 }]
const lightbox = new PhotoSwipeLightbox({ dataSource: image, index: index, pswpModule: () => import("photoswipe") })
lightbox.init()
lightbox.loadAndOpen(index)
}
意味がわからんと思われますでしょうが、まず解説します。
拡大アイコンが押された際にその場所の画像を拡大するため、拡大が押された瞬間に新しくhotoSwipeLightboxを生成しています。
先程デフォルトのクリックイベントを削除したため、loadAndOpen()で直接の画像を拡大表示しています。
最後の以外自分でも何してるかよくわかってません。
ただSimpleGallery内で同様にloadAndOpen関数を呼び出す記述を書き、handleExpandClick内でrefを経由して実行するコードも書きましたがうまく行きませんでした。
また後述しますが、hotoswipe/style.cssをインポートしてclassNameがpswp-galleryのdivタグでラップしないとcssが適応されず、背景が暗転せず真っ白になります。それをわかっていなかったため、呼び出し元でPhotoSwipeLightboxをnewしているにも関わらず、SimpleGalleryコンポーネントで画像をラップしないと表示されない状況にあったため、このような不可解なコードができました。
問題発生
上の文章を読まれた方は既にお気づきかもしれませんが、現在のコードではひとつひとつの画像をPhotoSwipeLightboxに渡しているため、ある問題が起こっています。
それはデフォルトでついていた画像の切り替えが、拡大表示画面でできなくなっています。
photoswipeでは渡された画像一覧からギャラリーを作成されるため、横にスワイプなどするには当然つながる画像が全て事前に渡されていないといけません。
ということでhandleExpand関数内でlightboxを初期化する方法が使えなくなり、ほぼ振り出しに戻りました。
しかしながらこの紆余曲折を経てようやく少しだけこのライブラリを理解できたので、次でようやく解決します。
目標達成
ここまで上記のように何もコードをちゃんと読まず脳死で行っていましたが。最後にようやくコードを理解して問題を解決しました。
Galleryを使っているindexではなく、SimpleGallery.jsの構造を紐解いていきました。
このコードではuseEffectを用いて初回レンダリング時にPhotoSwipeLightbox型のlightbox変数を作成し初期化しています。
しかしコンポーネントとして返される部分ではlightbox変数は使われていません。
またimagesによって形成される部分はaタグやimgタグで構成されていて至って普通です。そこで一番親のタグであるdivタグのclassNameに注目し、この部分によってlightboxを適応していると考えました。(当たり前じゃんと思われるかもしれませんが、割とひらめいた気持ちでした)
<div className="pswp-gallery" id={props.galleryID}>
</div>
そこで今度はSimpleGalleryを呼び出すのではなく、元のページでlightboxを定義するように変更しました。
先程のコードで生成AIに書かせた時、optionのdataSource経由で画像のデータを渡せる事を知りました。
そこでlightboxのStateを用意して、useEffectに下記のように記述をします。
const [lightbox, setLightbox] = useState<PhotoSwipeLightbox | null>(null)
useEffect(() => {
if (!className || !dataImages[className]) return
const initializeLightbox = async () => {
const lightboxInstance = new PhotoSwipeLightbox({
dataSource: await Promise.all(
dataImages[className].map(async (image) => {
const originalSize = await getImageSize(image.image_url || "")
return {
src: image.image_url || "",
w: originalSize.width,
h: originalSize.height,
}
}) || [],
),
pswpModule: () => import("photoswipe"),
arrowPrev: true, // 左矢印キーを有効にする
arrowNext: true, // 右矢印キーを有効にする
})
console.log("lightboxInstance", lightboxInstance)
lightboxInstance.init()
setLightbox(lightboxInstance)
return () => {
lightboxInstance.destroy()
setLightbox(null)
}
}
initializeLightbox()
}, [modalClassName, notificationImages])
またreturnする画像をSimpleGalleyからImageに戻して、pswp-galleryクラスのdivタグでラップします。
<div className="pswp-gallery" id={"my-gallery"} style={{ display: "contents" }}>
{ dataImages[className]?.map((mappedImage, index) => (
<div key={index}
{mappedImage.image_url ? (
<div>
<>
<Image
src={mappedImage.image_url}
alt="Card"
width={500}
height={300}
/>
</div>
...
</div>
かなり省略していますがこんな感じです。
またSimpleGalleyコンポーネントを使っていないので、import "photoswipe/style.css"文を忘れずに追加します。(一回忘れて真っ白になりました)
以上でdataに対するmap操作が増えてしまっているので、ベストプラクティスとは言えないかもしれませんが全ての要件を満たすことができました。
終わりに
長々と書いてきましたが、今回具体的な解決方法よりもどのように考えたときうまく行き、ちゃんと考えないとどのようにドツボにはまるのかを実感できるいい経験だったと感じたので記事にしました。
ですので問題を解決する記事としては使いづらいかもしれませんが、それでも役に立ったら幸いです。
今後このようなに考えなしにやって時間を無駄にしないよう、しっかり考えながら深く調べる癖をつけて沢山アウトプットしていきたいと思います。