現在RubyとJavaScriptを用いて記事投稿サイトを作成しています。
今回は新規投稿画面にて
#####「画像をドラッグ&ドロップで追加」
を実装していきたいと思います。
###■仕様等
・ActiveStorageを利用
・jQueryは非使用
・画像をドロップするとプレビュー画像が表示される
//⓪ 全体の処理を行う条件と要素を取得
if (document.URL.match( /new/ ) || document.URL.match( /edit/ )) {
document.addEventListener('DOMContentLoaded', function(){
const body = document.querySelector('body');
const imageList = document.getElementById("image-list");
const inputList = document.getElementById("input-list");
const firstInput = document.getElementById("input_article_image_-1");
firstInput.setAttribute('class', 'focus_input');
const dropText = document.getElementById('drop-text');
//② ドロップされた画像のプレビューと、次の画像に対応するinput要素の生成
//②-1 div要素の生成
const createImageHTML = (blob) => {
const imageElement = document.createElement('div');
imageElement.setAttribute('class', 'image-element');
let imageElementNum = document.querySelectorAll('.image-element').length
//②-2 プレビュー(img要素)の生成
const blobImage = document.createElement('img');
blobImage.setAttribute('src', blob);
blobImage.setAttribute('class', 'blob-image');
if (document.getElementsByClassName('article_image_input').length == 0 ) {
blobImage.setAttribute('id', 'article_image_-1');
} else {
blobImage.setAttribute('id', `article_image_${imageElementNum - 1}`);
}
//②-3 input要素の生成
const inputHTML = document.createElement('input');
inputHTML.setAttribute('id', `input_article_image_${imageElementNum}`);
inputHTML.setAttribute('name', 'article[images][]');
inputHTML.setAttribute('class', 'article_image_input')
inputHTML.setAttribute('type', 'file');
inputHTML.classList.add('focus_input');
//②-4 生成した各要素を挿入
imageElement.appendChild(blobImage);
inputList.appendChild(inputHTML)
imageList.appendChild(imageElement);
//②-5 入力済みのinput要素を非表示
if (document.getElementsByClassName('article_image_input').length == 1 ) {
firstInput.setAttribute('style', 'visibility: hidden; color: rgba(0,0,0,0);');
firstInput.classList.remove('focus_input');
} else {
document.getElementById(`input_article_image_${imageElementNum - 1}`).setAttribute('style', 'visibility: hidden; color: rgba(0,0,0,0);');
document.getElementById(`input_article_image_${imageElementNum - 1}`).classList.remove('focus_input');
}
//③ プレビューに対するアクション
//③-1 カーソルを乗せた時の処理
blobImage.addEventListener('mouseover', () => {
blobImage.setAttribute('style', "opacity: 0.4;")
});
blobImage.addEventListener('mouseout', () => {
blobImage.removeAttribute('style', "opacity: 0.4;")
});
//③-2 クリックした時の処理
blobImage.addEventListener('click', (e) => {
const targetInput = document.getElementById(`input_${blobImage.id}`);
console.log(targetInput.id)
targetInput.value = "";
if (!(targetInput.id == 'input_article_image_-1')) {
targetInput.remove();
};
blobImage.parentNode.setAttribute('style', 'display: none;')
blobImage.remove();
if (document.getElementsByClassName('article_image_input').length == 1 && firstInput.value == "" ) {
firstInput.nextElementSibling.remove();
firstInput.removeAttribute('style', 'visibility: hidden;');
firstInput.setAttribute('class', 'focus_input');
dropText.removeAttribute('style', 'display: none;');
}
});
};
//① 画像をドラッグ&ドロップした際の処理
//①-1 ドラッグしている時の処理
body.addEventListener('dragover', (e) => {
e.preventDefault();
imageList.setAttribute('style', 'background-color: rgba(206, 207, 196, 0.4); border: 3px solid lightblue;')
});
body.addEventListener('dragleave', (e) => {
e.preventDefault();
imageList.removeAttribute('style', 'background-color: rgba(206, 207, 196, 0.4); border: 3px solid lightblue;')
});
//①-2 ドロップした時の処理
body.addEventListener('drop', (e) => {
e.preventDefault();
imageList.removeAttribute('style', 'background-color: rgba(206, 207, 196, 0.4); border: 3px solid lightblue;')
dropText.setAttribute('style', 'display: none;');
const input = document.querySelector("input[class*='focus_input']");
input.files = e.dataTransfer.files;
const blob = window.URL.createObjectURL(input.files[0]);
createImageHTML(blob);
});
});
};
<%= form_with model: @article, url: articles_path, local: true, class: "new-article-form" do |f| %>
<%= render partial: "shared/error_message", locals: { model: f.object }, class: "error-messages"%>
<div id="image-area">
<div id="image-list">
<span id="drop-text">添付する画像をドロップ</span>
</div>
<div id="input-list">
<%= f.file_field :images, name: "article[images][]", id: "input_article_image_-1", class: "new-article-image" %>
</div>
</div>
###■実装手順(コード内の番号と対応)
#####⓪ 全体の処理を行う条件と要素を取得
#####① 画像をドラッグ&ドロップした際の処理
1- ドラッグしている時の処理
2- ドロップした時の処理
#####② ドロップされた画像のプレビューと、次の画像に対応するinput要素の生成
1- div要素を生成する
2- プレビュー(img要素)を生成
3- input要素を生成
4- 生成した各要素を挿入
5- 入力済みのinput要素を非表示
#####③ プレビューに対するアクション
1- カーソルを乗せた時の処理
2- クリックした時の処理
###■⓪ 全体の処理を行う条件と要素を取得
if (document.URL.match( /new/ ) || document.URL.match( /edit/ )) {
document.addEventListener('DOMContentLoaded', function(){
const body = document.querySelector('body');
const imageList = document.getElementById("image-list");
const inputList = document.getElementById("input-list");
const firstInput = document.getElementById("input_article_image_-1");
firstInput.setAttribute('class', 'focus_input');
const dropText = document.getElementById('drop-text');
1行目/2行目
処理全体を実行する条件を記述しています。
「新規投稿と編集ページにおいてHTMLを読み込んだ後に処理を行う」という内容です。
3行目〜8行目
ここでは今回利用する要素を取得しています。
・body
- ページ全体
・image-list
(div要素) - img要素(プレビュー)を格納する要素
・input-list
(div要素) - input要素(ファイル選択ボタン)を格納する要素
・firstInput
(div要素) - 最初に存在するinput要素。class属性に「focus_input」を設定
・drop-text
(span要素) - 「添付する画像をドロップ」を表示する要素
###■① 画像をドラッグ&ドロップした際の処理
#####①-1 ドラッグしている時の処理
#####①-2 ドロップした時の処理
####①-1 ドラッグしている時の処理
body.addEventListener('dragover', (e) => {
e.preventDefault();
imageList.setAttribute('style', 'background-color: rgba(206, 207, 196, 0.4); border: 3px solid lightblue;')
});
body.addEventListener('dragleave', (e) => {
e.preventDefault();
imageList.removeAttribute('style', 'background-color: rgba(206, 207, 196, 0.4); border: 3px solid lightblue;')
});
1行目
body領域にファイルがドラッグされた際のイベントを記述していきます。
2行目
デフォルトの処理をキャンセルしています。
3行目
image-list
にstyle属性を付与しています。
ファイルがドラッグされるとimage-list領域の背景色が水色になり枠線が表示されます。
6行目
ドラッグが解除された時の処理を記述しています。
7行目
デフォルトの処理をキャンセルしています。
8行目
3行目で設定したimage-listのstyle属性を削除しています。
####①-2 ドロップした時の処理
body.addEventListener('drop', (e) => {
e.preventDefault();
imageList.removeAttribute('style', 'background-color: rgba(206, 207, 196, 0.4); border: 3px solid lightblue;')
dropText.setAttribute('style', 'display: none;');
const input = document.querySelector(".focus_input");
input.files = e.dataTransfer.files;
const blob = window.URL.createObjectURL(input.files[0]);
createImageHTML(blob);
});
1行目
body領域にファイルがドロップされた際のイベントを記述していきます。
2行目
デフォルトの処理をキャンセルしています。
3行目
image-list
にstyle属性を削除しています。
image-list領域の背景色と枠線を削除します。
4行目
「添付する画像をドロップ」を非表示化しています。
5行目
「class='focus_input'
」が設定されているinput要素(⓪の7行目)を取得しています。
6行目
ドロップされた画像を取得したinput要素.files
に代入しています
7行目
input.files
の画像情報をURL化して変数blob
に代入しています
8行目
後述の関数の実引数に変数blobを設定
###■② ドロップされた画像のプレビューと、次の画像に対応するinput要素の生成
#####②-1 div要素を生成する
#####②-2 プレビュー(img要素)を生成
#####②-3 input要素を生成
#####②-4 生成した各要素を挿入
#####②-5 入力済みのinput要素を非表示
####①-1 div要素を生成する
画像が二枚目、三枚目と選択される度に生成される要素は以下の3つです。
・div要素(img要素の親要素)
・img要素(プレビュー画像)
・input画像(入力情報)
div要素を生成する理由は、
各img要素、各input要素に**重複しない番号(ID)**を付与する為です。
この後にプレビュー画像であるimg要素を生成しますが、その親要素として生成します↓
<div id='image-element' class='image-element'>
<img>
</div>
↓選択解除によりimg要素(プレビュー画像)が消えても
<div id='image-element' class='image-elemant'>
</div>
このように親要素のdiv要素が残るようにしてあげます。
このdiv要素のlength
をもとにimg要素やinput要素のid属性に番号を付与すれば、
idの重複を避けることができます。(例:article_image__1、article_image_2...)
const createImageHTML = (blob) => {
const imageElement = document.createElement('div');
imageElement.setAttribute('class', 'image-element');
let imageElementNum = document.querySelectorAll('.image-element').length
1行目
ドロップされた画像に対応するプレビュー及び、
次にドロップされるであろう画像に対応するinput要素を生成するための関数を宣言しています。
2行目
div要素を生成しています。
3行目
class属性を追加しています。
4行目
このdiv要素の総数を取得しています。
####②-2 プレビュー(img要素)を生成
const blobImage = document.createElement('img');
blobImage.setAttribute('src', blob);
blobImage.setAttribute('class', 'blob-image');
if (document.getElementsByClassName('article_image_input').length == 0 ) {
blobImage.setAttribute('id', 'article_image_-1');
} else {
blobImage.setAttribute('id', `article_image_${imageElementNum - 1}`);
}
1行目
img要素を生成しています。
2行目
src属性に①-2の7行目で作成したURLを設定しています。
3行目
class属性を追加しています。
4行目~7行目
id属性を追加しています(後述の、追加のinput要素が存在するか否かで条件分岐)
- 追加のinput要素が存在しない時(4行目)
article_image-1
を設定(5行目) - 追加のinput要素が存在する時(6行目)
article_image_${imageElementNum - 1}
を設定(7行目)
####②-3 input要素を生成
const inputHTML = document.createElement('input');
inputHTML.setAttribute('id', `input_article_image_${imageElementNum}`);
inputHTML.setAttribute('name', 'article[images][]');
inputHTML.setAttribute('class', 'article_image_input')
inputHTML.classList.add('focus_input');
inputHTML.setAttribute('type', 'file');
1行目
次にドロップされるであろう画像に対応するinput要素を生成しています。
2行目
id属性を設定しています。
次の画像に対応する為、次の番号を付与(=「-1」が付かない)
3行目
name属性を設定しています。
4行目
class属性を設定しています。
5行目
class属性を追加しています。
入力待ちのinput要素であることを識別する為の値
6行目
type属性を設定しています。
####②-4 生成した各要素を挿入
imageElement.appendChild(blobImage);
imageList.appendChild(imageElement);
inputList.appendChild(inputHTML)
1行目
img要素をdiv要素に挿入しています。
2行目
div要素をimage-list
に挿入しています。
3行目
input要素をinput-list
に挿入しています。
####②-5 入力済みのinput要素を非表示
if (document.getElementsByClassName('article_image_input').length == 1 ) {
firstInput.setAttribute('style', 'visibility: hidden; color: rgba(0,0,0,0);');
firstInput.classList.remove('focus_input');
} else {
document.getElementById(`input_article_image_${imageElementNum - 1}`).setAttribute('style', 'visibility: hidden; color: rgba(0,0,0,0);');
document.getElementById(`input_article_image_${imageElementNum - 1}`).classList.remove('focus_input');
}
1行目〜6行目
追加のinput要素が1つの場合の処理を記述しています(= 最初の画像がドロップされた直後)
- 最初のinput要素を非表示(2行目)
- 最初のinput要素から入力待ちを意味する「
class:focus_input
」を削除(3行目)
追加のinput要素が2つ以上の場合の処理を記述しています(4行目)
- 直前のinput要素に対して上と同様の処理(5行目/6行目)
###■③ プレビューに対するアクション
#####③-1 カーソルを乗せた時の処理
#####③-2 クリックした時の処理
####③-1 カーソルを乗せた時の処理
blobImage.addEventListener('mouseover', () => {
blobImage.setAttribute('style', "opacity: 0.4;")
});
blobImage.addEventListener('mouseout', () => {
blobImage.removeAttribute('style', "opacity: 0.4;")
});
プレビューにカーソルを乗せた際に、プレビューを半透明化しています。
####③-2 クリックした時の処理
blobImage.addEventListener('click', () => {
const targetInput = document.getElementById(`input_${blobImage.id}`);
targetInput.value = "";
if (!(targetInput.id == 'input_article_image_-1')) {
targetInput.remove();
};
blobImage.parentNode.setAttribute('style', 'display: none;')
blobImage.remove();
if (document.getElementsByClassName('article_image_input').length == 1 && firstInput.value == "" ) {
firstInput.nextElementSibling.remove();
firstInput.removeAttribute('style', 'visibility: hidden;');
firstInput.setAttribute('class', 'focus_input');
dropText.removeAttribute('style', 'display: none;');
}
});
1行目
プレビューがクリックされた際のイベントを記述していきます。
2行目/3行目
クリックされたimg要素に対応するinput要素を取得して代入しています。(2行目)
そのinput要素の値を削除しています。(3行目)
4行目〜6行目
最初のinput要素ではない場合(=2つ目以降のinput要素の場合)、(4行目)
そのinput要素自体を削除しています。(5行目)
7行目
img要素の親要素であるdiv要素を非表示にしています。
8行目
img要素を削除しています。
9行目〜14行目
以降はプレビュー画像をクリックして選択を解除した際、
それが最後の一枚で、画像が全て解除された場合の処理を記述しています。
求める処理結果としては、最初のinput要素を再表示させて入力待ちの状態に戻し、
「添付する画像をドロップ」メッセージを再表示する事です。
追加されたinput要素が一つ且つ最初のinput要素が空の場合、(9行目)
追加されたinput要素を削除しています。(10行目)
非表示となっていた最初のinput要素を表示しています。(11行目)
最初のinput要素に入力待ちを意味する「class:focus_input
」を追加しています。(12行目)
「添付する画像をドロップ」を表示しています。(13行目)
以上で完成です。
####参考記事
https://kuwk.jp/blog/dd/