画像のプレビューを複数枚表示できるように実装していきたいと思います。
複数枚表示するだけではなく、各画像を変更したり、削除できるようにも実装していきます。
前回の記事に続き実装をしていきます。
プレビュー機能の記事を読んでいない方は、そちらを先に御覧ください。
目次
複数枚プレビュー表示機能
それでは、複数枚の画像のプレビューを表示できるように実装していきましょう。
index.htmlとpreview.jsを以下のように編集しましょう。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Preview</title>
<link rel="stylesheet" href="style.css">
<script src="preview.js"></script>
</head>
<body>
<div id="preview-box"></div>
<!-- file_fieldを追加で表示させるために、divタグを追加 -->
<div class="file-upload">
<input type="file" id="preview" data-index="0">
</div>
</body>
</html>
window.addEventListener("DOMContentLoaded", () => {
const preview = document.getElementById("preview");
preview.addEventListener("change", (e) => {
const file = e.target.files[0];
const blob = window.URL.createObjectURL(file);
const imageList = document.querySelector(".preview-img");
if (imageList) {
imageList.remove();
}
const previewImage = document.createElement("img");
previewImage.setAttribute("src", blob);
previewImage.setAttribute("class", "preview-img");
const previewBox = document.getElementById("preview-box");
previewBox.appendChild(previewImage);
// 新しいfile_fieldを作成
const newFileField = document.createElement("input");
newFileField.setAttribute("type", "file");
// file_fieldを追加
const fileFields = document.querySelector(".file-upload");
fileFields.appendChild(newFileField);
})
});
index.htmlでは、JavaScriptで生成したfile_fieldを、追加で表示させるためのdivタグを追加しています。
preview.jsでは、新たなfile_fieldを生成し、index.htmlで追加したdivタグにappendしています。
以下のようにfile_filedが2つ表示されていればOKです。
表示させることができましたが、preview.jsで生成したfile_feildにはdata-indexを付与していません。
file_fieldとプレビュー画像にdata-indexを割り振っていきます。
data-index割り振り(プレビュー画像)
まずは、data-indexを取得する処理を追加します。
window.addEventListener("DOMContentLoaded", () => {
const preview = document.getElementById("preview");
preview.addEventListener("change", (e) => {
const file = e.target.files[0];
const blob = window.URL.createObjectURL(file);
const imageList = document.querySelector(".preview-img");
// 選択したfile_fieldのdata-indexを取得、確認
const dataIndex = e.target.getAttribute("data-index");
console.log(dataIndex);
if (imageList) {
imageList.remove();
}
const previewImage = document.createElement("img");
previewImage.setAttribute("src", blob);
previewImage.setAttribute("class", "preview-img");
const previewBox = document.getElementById("preview-box");
previewBox.appendChild(previewImage);
const newFileField = document.createElement("input");
newFileField.setAttribute("type", "file");
const fileFields = document.querySelector(".file-upload");
fileFields.appendChild(newFileField);
})
});
動作確認して検証ツールのconsoleに「0」が表示されるか確認しましょう。
次に取得したdata-indexを使って、プレビュー画像にも同じ値を割り振ります。
window.addEventListener("DOMContentLoaded", () => {
const preview = document.getElementById("preview");
preview.addEventListener("change", (e) => {
const file = e.target.files[0];
const blob = window.URL.createObjectURL(file);
const imageList = document.querySelector(".preview-img");
const dataIndex = e.target.getAttribute("data-index");
if (imageList) {
imageList.remove();
}
const previewImage = document.createElement("img");
previewImage.setAttribute("src", blob);
previewImage.setAttribute("class", "preview-img");
// プレビュー画像にdata-indexを割り振り
previewImage.setAttribute("data-index", dataIndex);
const previewBox = document.getElementById("preview-box");
previewBox.appendChild(previewImage);
const newFileField = document.createElement("input");
newFileField.setAttribute("type", "file");
const fileFields = document.querySelector(".file-upload");
fileFields.appendChild(newFileField);
})
});
編集することができたら、プレビュー画像にdata-indexが割り振られているか確認しましょう。
data-index割り振り(file_field)
次にpreview.jsで生成したfile_fieldにdata-indexを割り振っていきます。
file_fieldにdata-indexを割り振る際は、data-index+1にしておきます。
window.addEventListener("DOMContentLoaded", () => {
const preview = document.getElementById("preview");
preview.addEventListener("change", (e) => {
const file = e.target.files[0];
const blob = window.URL.createObjectURL(file);
const imageList = document.querySelector(".preview-img");
const dataIndex = e.target.getAttribute("data-index");
if (imageList) {
imageList.remove();
}
const previewImage = document.createElement("img");
previewImage.setAttribute("src", blob);
previewImage.setAttribute("class", "preview-img");
previewImage.setAttribute("data-index", dataIndex);
const previewBox = document.getElementById("preview-box");
previewBox.appendChild(previewImage);
const newFileField = document.createElement("input");
newFileField.setAttribute("type", "file");
// file_fieldにdata-indexを設定
const lastFileField = document.querySelector('input[type="file"]:last-child');
const nextDataIndex = Number(lastFileField.getAttribute("data-index")) + 1;
newFileField.setAttribute("data-index", nextDataIndex);
const fileFields = document.querySelector(".file-upload");
fileFields.appendChild(newFileField);
})
});
行ったことは以下になります。
- 表示されているfile_fieldを取得
- 1で取得したfile_fieldのdata-indexを取得し、+1する
- newFileFieldにdata-indexをセット
file_fieldにもdata-indexが割り振られているか確認しましょう。
処理を関数に分ける
イベントの中の処理が複雑になってきたので、関数に分けていきたいと思います。
以下のように置き換えてみましょう。
window.addEventListener("DOMContentLoaded", () => {
// プレビュー表示に関する処理
const buildPreview = (blob, dataIndex) => {
const previewImage = document.createElement("img");
previewImage.setAttribute("src", blob);
previewImage.setAttribute("class", "preview-img");
previewImage.setAttribute("data-index", dataIndex);
const previewBox = document.getElementById("preview-box");
previewBox.appendChild(previewImage);
}
// file_filedに関する処理
const buildFileField = () => {
const newFileField = document.createElement("input");
newFileField.setAttribute("type", "file");
const lastFileField = document.querySelector('input[type="file"]:last-child');
const nextDataIndex = Number(lastFileField.getAttribute("data-index")) + 1;
newFileField.setAttribute("data-index", nextDataIndex);
const fileFields = document.querySelector(".file-upload");
fileFields.appendChild(newFileField);
}
// イベントが発火したときの処理
const changeFileField = (e) => {
const file = e.target.files[0];
const blob = window.URL.createObjectURL(file);
const imageList = document.querySelector(".preview-img");
const dataIndex = e.target.getAttribute("data-index");
if (imageList) {
imageList.remove();
}
// プレビュー機能呼び出し
buildPreview(blob, dataIndex);
// file_filedに関する処理呼び出し
buildFileField();
}
// file_filedの取得
const preview = document.getElementById("preview");
// イベント発火、changeFileField呼び出し
preview.addEventListener("change", changeFileField);
});
プレビューに関する処理は、buildPreviewとして切り出しました。
file_fieldに関する処理は、buildFileFieldとして切り出しました。
イベント発火の処理は、changeFileFieldとして切り出しました。
関数に分けることで可読性の向上、コードの再利用ができるようになります。
プレビュー表示(2枚目以降)
現状では、2つめのfile_filedをクリックしてもプレビュー画像は表示されません。
preview.jsで生成したfile_fieldに対してもイベントが発火するようにします。
その際に、プレビュー画像があれば削除するという記述があると一枚目の画像が消えてしまうため、コードを削除します。
window.addEventListener("DOMContentLoaded", () => {
// プレビュー表示に関する処理
const buildPreview = (blob, dataIndex) => {
const previewImage = document.createElement("img");
previewImage.setAttribute("src", blob);
previewImage.setAttribute("class", "preview-img");
previewImage.setAttribute("data-index", dataIndex);
const previewBox = document.getElementById("preview-box");
previewBox.appendChild(previewImage);
}
// file_filedに関する処理
const buildFileField = () => {
const newFileField = document.createElement("input");
newFileField.setAttribute("type", "file");
const lastFileField = document.querySelector('input[type="file"]:last-child');
const nextDataIndex = Number(lastFileField.getAttribute("data-index")) + 1;
newFileField.setAttribute("data-index", nextDataIndex);
// 2枚目以降のプレビュー表示
newFileField.addEventListener("change", changeFileField);
const fileFields = document.querySelector(".file-upload");
fileFields.appendChild(newFileField);
}
// イベントが発火したときの処理
const changeFileField = (e) => {
const file = e.target.files[0];
const blob = window.URL.createObjectURL(file);
const dataIndex = e.target.getAttribute("data-index");
// ※ここから削除※
// const imageList = document.querySelector(".preview-img");
// if (imageList) {
// imageList.remove();
// }
// ※ここまで削除※
// プレビュー機能呼び出し
buildPreview(blob, dataIndex);
// file_filedに関する処理呼び出し
buildFileField();
}
// file_filedの取得
const preview = document.getElementById("preview");
// イベント発火、changeFileField呼び出し
preview.addEventListener("change", changeFileField);
});
buildFileField関数内に、newFileField.addEventListener("change", changeFileField)
を追記します。
これにより2枚目以降の画像もプレビュー表示することが可能になりました。
動作確認して上記のように表示させることができたでしょうか。
プレビュー画像の編集
現状すでに選択済みのfile_fieldをクリックし、画像を選択すると新たなプレビュー画像が追加されてしまいます。
本来なら画像が差し替わってほしいのですが、画像の追加処理が動いてしまうためこの様な状態になっています。
すでにプレビュー画像が表示されているのであれば画像の差し替えを行なうようにしていきます。
window.addEventListener("DOMContentLoaded", () => {
// プレビュー表示に関する処理
const buildPreview = (blob, dataIndex) => {
const previewImage = document.createElement("img");
previewImage.setAttribute("src", blob);
previewImage.setAttribute("class", "preview-img");
previewImage.setAttribute("data-index", dataIndex);
const previewBox = document.getElementById("preview-box");
previewBox.appendChild(previewImage);
}
// file_filedに関する処理
const buildFileField = () => {
const newFileField = document.createElement("input");
newFileField.setAttribute("type", "file");
// file_fieldにdata-indexを設定
const lastFileField = document.querySelector('input[type="file"]:last-child');
const nextDataIndex = Number(lastFileField.getAttribute("data-index")) + 1;
newFileField.setAttribute("data-index", nextDataIndex);
// 2枚目以降のプレビュー表示
newFileField.addEventListener("change", changeFileField)
// file_filedを表示するdivタグを取得、append
const fileFields = document.querySelector(".file-upload");
fileFields.appendChild(newFileField);
}
// イベントが発火したときの処理
const changeFileField = (e) => {
const file = e.target.files[0];
const blob = window.URL.createObjectURL(file);
const dataIndex = e.target.getAttribute("data-index");
// data-indexを使ってプレビューが表示されているか確認
const oldPreview = document.querySelector(`.preview-img[data-index="${dataIndex}"]`)
if (oldPreview) {
// すでにプレビュー画像が表示されている場合の処理
oldPreview.setAttribute("src", blob);
return null;
}
// プレビュー機能呼び出し
buildPreview(blob, dataIndex);
// file_filedに関する処理呼び出し
buildFileField();
}
// file_filedの取得
const preview = document.getElementById("preview");
// イベント発火、changeFileField呼び出し
preview.addEventListener("change", changeFileField);
});
changeFileField関数内に、すでにプレビュー画像が表示されていた際の処理を記述しました。
画像が差し替わるか、動作確認をしてみましょう。
画像の選択をキャンセルしたときの処理
すでに選択済みのfile_fieldを再度選択し、キャンセルを押すとプレビュー画像が残ったままになってしまいます。
file_filedの中身は空なのにプレビューが表示されているのはおかしいので削除するようにします。
window.addEventListener("DOMContentLoaded", () => {
// プレビュー表示に関する処理
const buildPreview = (blob, dataIndex) => {
const previewImage = document.createElement("img");
previewImage.setAttribute("src", blob);
previewImage.setAttribute("class", "preview-img");
previewImage.setAttribute("data-index", dataIndex);
const previewBox = document.getElementById("preview-box");
previewBox.appendChild(previewImage);
}
// file_filedに関する処理
const buildFileField = () => {
const newFileField = document.createElement("input");
newFileField.setAttribute("type", "file");
// file_fieldにdata-indexを設定
const lastFileField = document.querySelector('input[type="file"]:last-child');
const nextDataIndex = Number(lastFileField.getAttribute("data-index")) + 1;
newFileField.setAttribute("data-index", nextDataIndex);
// 2枚目以降のプレビュー表示
newFileField.addEventListener("change", changeFileField);
// file_filedを表示するdivタグを取得、append
const fileFields = document.querySelector(".file-upload");
fileFields.appendChild(newFileField);
}
// すでに選択済みのfile_filedを再度選択し、何も画像を選択しなかった場合の処理
const deletePreview = (dataIndex) => {
// プレビュー画像削除
const previewImage = document.querySelector(`.preview-img[data-index="${dataIndex}"]`);
previewImage.remove();
// file_filed削除
const fileField = document.querySelector(`input[type="file"][data-index="${dataIndex}"]`);
fileField.remove();
}
// イベントが発火したときの処理
const changeFileField = (e) => {
const dataIndex = e.target.getAttribute("data-index");
const file = e.target.files[0];
// fileが空の場合(何も選択されなかった場合)、プレビュー等を削除する
if (!file) {
deletePreview(dataIndex);
return null;
}
const blob = window.URL.createObjectURL(file);
const oldPreview = document.querySelector(`.preview-img[data-index="${dataIndex}"]`)
if (oldPreview) {
oldPreview.setAttribute("src", blob);
return null;
}
// プレビュー機能呼び出し
buildPreview(blob, dataIndex);
// file_filedに関する処理呼び出し
buildFileField();
}
// file_filedの取得
const preview = document.getElementById("preview");
// イベント発火、changeFileField呼び出し
preview.addEventListener("change", changeFileField);
});
deletePreview関数を定義し、changeFileField関数内で呼び出します。
fileの中身が空の場合に呼び出されるようにしています。
何も選択せずウインドウを閉じるとプレビュー等が削除されるか確認してみましょう。
削除ボタンの実装
プレビュー画像の下に削除ボタンを配置して、削除ボタンから画像を削除できるようにしてみます。
style.cssに以下の記述を追加しましょう。
.delete-button {
width: 250px;
text-align: center;
border: solid 1px;
cursor: pointer;
}
次に削除ボタンの生成と処理を記述していきます。
手順は以下のようになります。
- buildPreview関数内で削除ボタンを作成、表示
- 削除ボタンがクリックされたら、deletePreview関数を呼び出し実行
上記の内容を踏まえた上で、完成コードを載せておきます。
window.addEventListener("DOMContentLoaded", () => {
// プレビュー表示に関する処理
const buildPreview = (blob, dataIndex) => {
const previewImage = document.createElement("img");
previewImage.setAttribute("src", blob);
previewImage.setAttribute("class", "preview-img");
previewImage.setAttribute("data-index", dataIndex);
// 削除ボタンの生成
const deleteButton = document.createElement("div");
deleteButton.setAttribute("class", "delete-button");
deleteButton.setAttribute("data-index", dataIndex);
deleteButton.innerText = "削除";
// 削除ボタンをクリックしたら、プレビューとfile_filedを削除
deleteButton.addEventListener("click", () => deletePreview(dataIndex));
// プレビュー画像一覧表示
const previewBox = document.getElementById("preview-box");
previewBox.appendChild(previewImage);
// 削除ボタンをappend
previewBox.appendChild(deleteButton);
}
// file_filedに関する処理
const buildFileField = () => {
const newFileField = document.createElement("input");
newFileField.setAttribute("type", "file");
// file_fieldにdata-indexを設定
const lastFileField = document.querySelector('input[type="file"]:last-child');
const nextDataIndex = Number(lastFileField.getAttribute("data-index")) + 1;
newFileField.setAttribute("data-index", nextDataIndex);
// 2枚目以降のプレビュー表示
newFileField.addEventListener("change", changeFileField);
// file_filedを表示するdivタグを取得、append
const fileFields = document.querySelector(".file-upload");
fileFields.appendChild(newFileField);
}
// すでに選択済みのfile_filedを再度選択し、何も画像を選択しなかった場合の処理
const deletePreview = (dataIndex) => {
// プレビュー画像削除
const previewImage = document.querySelector(`.preview-img[data-index="${dataIndex}"]`);
previewImage.remove();
// file_filed削除
const fileField = document.querySelector(`input[type="file"][data-index="${dataIndex}"]`);
fileField.remove();
// 削除ボタン削除
const deleteButton = document.querySelector(`.delete-button[data-index="${dataIndex}"]`);
deleteButton.remove();
}
// イベントが発火したときの処理
const changeFileField = (e) => {
// 何番目のfile_fieldを操作しているか確認
const dataIndex = e.target.getAttribute("data-index");
const file = e.target.files[0];
// fileが空の場合(何も選択されなかった場合)、プレビュー等を削除する
if (!file) {
deletePreview(dataIndex);
return null;
}
const blob = window.URL.createObjectURL(file);
// data-indexを使ってプレビューが表示されているか確認
const oldPreview = document.querySelector(`.preview-img[data-index="${dataIndex}"]`)
if (oldPreview) {
// すでにプレビュー画像が表示されている場合の処理
oldPreview.setAttribute("src", blob);
return null;
}
// プレビュー機能呼び出し
buildPreview(blob, dataIndex);
// file_filedに関する処理呼び出し
buildFileField();
}
// file_filedの取得
const preview = document.getElementById("preview");
// イベント発火、changeFileField呼び出し
preview.addEventListener("change", changeFileField);
});
最後に動作確認をしておきましょう。
以上で、複数枚のプレビュー画像表示、編集、削除機能は終了になります。
いかがだったでしょうか。
JavaScriptの記述が少し複雑で難しく感じた方もいたかもしれません。
何度か実装してみることで理解することが可能かと思います。
ぜひご自身のアプリなどに組み込んでみてください。
最後までご覧頂きありがとうございました。