【PHP, JavaScript】画像を並び替えてアップロードする【前編】

## 今回作ったもの
（1）アップロードしたい画像（1枚～4枚まで）を選択すると、その場でプレビューします。

<img width="400" alt="2023-11-20 181323.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118500/32ef89b8-3930-44ff-e72e-ecd8b75f24f3.png">

（2）ドラッグ＆ドロップで画像の位置を入れ替えます。
（並び替え後↓↓）

<img width="400" alt="2023-11-20 181751.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118500/15ffd379-969a-6e56-dc4c-6365f84d0e91.png">

（3）「投稿」ボタンを押してアップロード。順番は崩れません。

<img width="400" alt="2023-11-20 201508.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118500/b2cf7f05-0da5-54df-51fd-3541757e600c.png">

さらに、**画像の枚数に応じたレイアウトを自動で適用**します。
* 1枚 --> 2枚 --> 3枚
X（旧Twitter）で画像を投稿した時のような感じにしてみました。

<img width="300" alt="2023-11-20 211217.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118500/d972d70a-a663-0bf5-a7f1-34007babec39.png">

<img width="300" alt="2023-11-20 211259.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118500/0d8c4592-75f0-6635-7a91-fb13f9ddfad0.png">

<img width="300" alt="2023-11-20 211334.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118500/865bd726-a703-7fb6-e551-3a0061e7b5d6.png">

## 作成するファイル
* [upload.html【完成】](#uploadhtml完成)
* [photos_view.css【完成】](#photos_viewcss完成)
* [preview.js【完成】](#previewjs完成)
* upload_process.php【完成】---【後編で作成】

## 今回やること
* [（1）HTMLファイルの作成](#1htmlファイルの作成)
* [（2）画像の枚数に応じたCSSを自動で適用](#2画像の枚数に応じたcssを自動で適用)
* [（3）プレビュー＆位置情報を保存](#3プレビュー位置情報を保存)
[①全ての\<img>を作る](#全てのimgを作る)
[②枚数分の\<div>を作り、\<img>を先着順に入れていく](#枚数分のdivを作りimgを先着順に入れていく)
* [（4）ドラッグ＆ドロップ](#4ドラッグドロップ)
 [①必要な要素を取得する](#必要な要素を取得する)
 [②画像を入れ替える](#画像を入れ替える)
 [③位置情報を保存](#位置情報を保存)
* [（5）JavaScriptで作った位置情報を\<form>で送信できるようにする](#5javascriptで作った位置情報をformで送信できるようにする)

```preview.js
'use strict';
const photo = document.getElementById('photo');
const upload = document.getElementById('upload');
const photoOrder = [];

photo.addEventListener("change", function(){
//****************************（2）*******************************
  const numberOfPhoto = this.files.length;
  const allPhotos = document.getElementById('all-photos');
  allPhotos.classList.add('layout-' + numberOfPhoto);
//****************************（3）**********************************
//****************************①**************************************
  const selectedPhotos = [];
  const promise = new Promise((resolve) => {
    for(let i = 0; i < numberOfPhoto; i++){
      const fileReader = new FileReader();
      fileReader.readAsDataURL(this.files[i]);
      fileReader.addEventListener("load", function(){
        const img = document.createElement('img');
        img.src = this.result;
        img.id = 'photo-' + i;
        selectedPhotos.push(img);
        photoOrder.push(i);
        if(selectedPhotos.length == numberOfPhoto){
          resolve(selectedPhotos);
        }
      });
    }
  });
//****************************②**************************************
  promise.then((selectedPhotos) => {
    for(let i = 0; i < selectedPhotos.length; i++){
      const img = selectedPhotos[i];
      const div = document.createElement('div');
      div.id = 'div-' + i;
      div.appendChild(img);
      allPhotos.appendChild(div);
//****************************（4）***********************************
      img.addEventListener('dragstart', function(event){
        event.dataTransfer.setData("text/plane", img.id);
      });
      img.addEventListener('dragover', function(event){
        event.preventDefault();
      });
      img.addEventListener('dragenter', function(event){
        event.preventDefault();
      });
      img.addEventListener('drop', function(event){
      //****************************（①）***********************************
        const dragImgId = event.dataTransfer.getData("text/plane");
        const dragImg = document.getElementById(dragImgId);
        function callbackFn(element){
          if(element === this){
            return true;
          }
          return false;
        }
        const dragImgOrder = photoOrder.findIndex(callbackFn, parseInt(dragImg.id.slice(6)));
        const dropImgOrder = photoOrder.findIndex(callbackFn, parseInt(img.id.slice(6)));
        const dragImgDiv = document.getElementById('div-' + dragImgOrder);
        const dropImgDiv = document.getElementById('div-' + dropImgOrder);
        //****************************（②）***********************************
        dragImgDiv.appendChild(img);
        dropImgDiv.appendChild(dragImg);
        //****************************（③）***********************************
        photoOrder[dragImgOrder] = parseInt(img.id.slice(6));
        photoOrder[dropImgOrder] = parseInt(dragImg.id.slice(6));
      });
    }
  });
});
//****************************（5）***********************************
upload.addEventListener("submit", function(){
  const json = JSON.stringify(photoOrder);
  const input = document.createElement('input');
  input.type = "hidden";
  input.name = "photo-order";
  input.value = json;
  upload.appendChild(input);
});
```
長いですが、分けて1つずつ説明します。
## （1）HTMLファイルの作成
選択した画像をプレビューするHTMLの構造です。
```html:upload.html
  <body>
    <div id="photos-view">
      <div id="all-photos">
      <!--JavaScriptで<div>と<img>を生成-->
      </div>
    </div>
    <form></form>
  <script src="./js/preview.js"></script>
  </body>
```
all-photosに全ての画像が入ります。1枚かもしれないし、4枚かもしれません。なのでJavaScriptで**画像の枚数分の\<div>と\<img>** を生成します（次項の[（3）プレビュー＆位置情報を保存](#3プレビュー位置情報を保存)で）。例えば画像が4枚なら、最終的に以下の構造となり、画像をプレビューします。
```html:upload.html
  <body>
    <div id="photos-view">
        <div id="all-photos">
          <div id="" ><img id="" src="" ></div>
          <div id="" ><img id="" src="" ></div>
          <div id="" ><img id="" src="" ></div>
          <div id="" ><img id="" src="" ></div>
        </div>
    </div>
    <form></form>
  <script src="./js/preview.js"></script>
  </body>
```
次に\<form>を作ります。action属性にはupload_process.phpを指定します。

### upload.html【完成】
```html:upload.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link rel="stylesheet" href="./css/photos_view.css"/>
  </head>
  <body>
    <div id="photos-view">
      <div id="all-photos"></div>
    </div>
    <form id="upload" method="POST" enctype="multipart/form-data" action="./upload_process.php">
      <input type="hidden" name="max_file_size" value="1000000"/>
      <input id="photo" type="file" name="photo[]" size="50" multiple/>
      <input type="submit" value="投稿"/>
    </form>
  <script src="./js/preview.js"></script>
  </body>
</html>
```
* enctype属性は、フォームデータのエンコード形式を指定する。これがないとサーバ側で正しくデータを受け取ることができない。
* `name="max_file_size"`では、アップロード可能なファイルサイズの上限（バイト単位）を指定。ここでは1MBだが各自調節を。今回一度で複数のファイルをアップロードするので、総ファイルサイズの上限かと思ったが、試してみると「1ファイルあたり1MBまで」の挙動をした。悪意のあるユーザーが`max_file_size`を改ざんすることは簡単なので、紳士協定的な設定と考えるべきだが一応設定を。**必ず\<input type="file"/>の前に書くこと。**
* アップロードするファイルを選ぶには\<input type="file"/>。これでは1つのファイルしか選択できない。一度に複数のファイルをアップロードできるようにするには、**name属性に[]とmultiple属性を必ず書く。**
* size="50"は入力欄の幅(50文字分)。

【参考】
・『独習PHP 第4版』　山田祥寛著

## （2）画像の枚数に応じたCSSを自動で適用
ここからJavaScriptで、画像をプレビューするための機能を作っていきます。
アップロードしたい画像を選択すると、「選択したファイルの情報」がFileListオブジェクトに格納されます。このFileListオブジェクトが欠かせません。
* FileListオブジェクトはchangeイベント内で確認。以下はどれも同じ結果が得られますが、本記事では「this.files」とします。
```javascript:preview.js
const photo = document.getElementById('photo');//(1)の<input type="file">を取得
photo.addEventListener("change", function(event){
  console.log(this.files);
  console.log(photo.files);
  console.log(event.target.files);
});
```
様々な縦横比の画像をきれいにまとめて表示するのはCSSです。画像の枚数に応じたレイアウトを自動で適用するには、JavaScriptで画像の枚数を読み取り、「layout-（枚数）」というclass属性を、（1）upload.htmlの「all-photos」に付けます。

```javascript:preview.js
const photo = document.getElementById('photo');//(1)の<input type="file">を取得
photo.addEventListener("change", function(){
  const numberOfPhoto = this.files.length;//画像の枚数
  const allPhotos = document.getElementById('all-photos');
  allPhotos.classList.add('layout-' + numberOfPhoto);
});
```
例えば、画像が4枚なら`layout-4`というclass属性が（1）upload.htmlの「all-photos」に割り当てられ、このようなHTML構造でプレビューすることになります。
```html:upload.html
    <div id="photos-view">
        <div id="all-photos" class="layout-4">
          <div id="" ><img id="" src="" ></div>
          <div id="" ><img id="" src="" ></div>
          <div id="" ><img id="" src="" ></div>
          <div id="" ><img id="" src="" ></div>
        </div>
    </div>
    <!--省略-->
```
`layout-4`には、中の画像を「2つ横に並べたら下に折り返す」というCSSを書きました。このように、`layout-1`から`layout-4`までCSSを用意します。枚数に対応する1つだけが適用されます。

### photos_view.css【完成】
```photos_view.css
body{
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
}
body #photos-view{
  margin-top: 8rem;
  width: 400px;
}
body #photos-view .layout-1, .layout-2, .layout-3, .layout-4{
  width: 100%;
  height: 32vh;
  display: flex;
  flex-wrap: wrap;
}
body #photos-view .layout-3{
  flex-direction: column;
}
/* 画像1枚の時(.layout-1) */
body #photos-view .layout-1 > div{
  width: 100%;
  height: 100%;
}
/* 画像2枚の時(.layout-2) */
body #photos-view .layout-2 > div{
  width: 50%;
  height: 100%;
}
/* 画像3枚の時(.layout-3) */
body #photos-view .layout-3 > div:first-child{
  width: 50%;
  height: 100%;
}
body #photos-view .layout-3 > div:not(:first-child){
  width: 50%;
  height: 50%;
}
/* 画像4枚の時(.layout-4) */
body #photos-view .layout-4 > div{
  height: 50%;
  width: 50%;
}
body #photos-view img{
  object-fit: cover;
  width: 100%;
  height: 100%;
}
```
次回作成するアップロード先のファイルでもHTMLの構造は全く同じなので、同じCSSファイルを使います。
## （3）プレビュー＆位置情報を保存
おおまかに説明します。
**プレビュー**では、for文を使って**画像の枚数分の\<div>と\<img> を作ります。** 
* \<div>に「div-○」というid属性、\<img>に「photo-○」というid属性を付与。○はループカウンタ変数`$i`で**連番を振る。**

<img width="400" alt="スクリーンショット 2023-11-21 173323.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118500/50e5f117-d0e2-fc6d-1c9c-a766c15ed4e2.png">

* \<img>は非同期処理になるので、バラバラの順番。その先着順で親要素\<div>に入れる。
* \<div>は同期処理で、常に順番通りで固定されていなければならない。\<img>をドラッグ＆ドロップしても入れ替わらない。

\<div>に\<img>を入れます。4枚の場合、中の画像を「2つ横に並べたら下に折り返す」というCSSを書いているので、こうなります。

<img width="400" alt="2023-11-21 163427.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118500/66a1d6b8-c9a9-e128-9668-283fd76274f4.png">

この時、画像の位置情報が分かります。
* div-0の所にphoto-3
* div-1の所にphoto-1
* div-2の所にphoto-2
* div-3の所にphoto-0

「どの\<div>にどの\<img>がはいっているか」、これを配列photoOrderに格納し、**位置情報を保存**します。`photoOrder = [3, 1, 2, 0];`という感じです。

<img width="400" alt="スクリーンショット 2023-12-01 110244.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118500/a4b56da1-b350-3c09-2779-3c7a00ee503a.png">

* \<img>に振った連番を格納
*  **\<div>に振った連番と配列のインデックスは一致する**

これをコードに書いていきます。
\<img>は非同期処理（バラバラの順）ですが、\<div>は同期処理（順番通り）でないといけないので、\<img>と\<div>は別々のfor文で作ります。ここでPromiseオブジェクトを使います。これで「この処理が終わった後に、この処理をする」という動きを作ることができます。promiseについては、[こちら](https://qiita.com/cheez921/items/41b744e4e002b966391a)の記事が分かりやすいです。
**「①全ての\<img>を作ったら、②枚数分の\<div>を作り、\<img>を先着順に入れていく」** という処理を書きます。

### ①全ての\<img>を作る
ここでもう一度FileListオブジェクトについて。
* \<input type="file"> で選択したファイルのリスト。
* 「ファイル名の昇順」に並ぶ（ブラウザによるかも？）
* 「全ての画像の情報」「Fileオブジェクトの集まり」。

Fileオブジェクトには、「1つの画像の情報」が格納されています。**\<img>を作るには、FileListオブジェクトの中のFileオブジェクトから生成します。**

<img width="400" alt="スクリーンショット 2023-11-28 204925.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118500/6f869698-03a8-a7d7-3030-543c8ede570a.png">

Fileオブジェクトにアクセスするために、FileReaderオブジェクトを使用します（これが非同期的にアクセスするため、バラバラの順番になる）。実際にファイルの内容を取得するのは、FileReaderオブジェクトが用意している「readAs○○」という名のメソッドです。src属性の値を取得するために、[readAsDataURLメソッド](https://developer.mozilla.org/ja/docs/Web/API/FileReader/readAsDataURL)を使います。今回、src属性の値は「 [データURL](https://developer.mozilla.org/ja/docs/Web/HTTP/Basics_of_HTTP/Data_URLs)」となります。これは小さなファイルをインラインで文書に埋め込むことができます。なのでインライン要素\<img>で使うことができます。

#### readAsDataURLメソッドを使うと・・・
* 取得したデータ（src属性の値。base64エンコーディングされた [データURL](https://developer.mozilla.org/ja/docs/Web/HTTP/Basics_of_HTTP/Data_URLs)の文字列）は**resultプロパティ**に格納される。
* readyStateプロパティが「2（DONE）」となり、loadedイベントが発生。

readyStateプロパティは、「読み取り操作の状態」を示します。これが「2」になります。
* 「readAs○○」メソッドはまだ呼び出されていない・・・「0」
* 「readAs○○」メソッドが呼び出され、読み取り中・・・「1」
* 操作完了（成功もエラーも含む）・・・「2」

loadedイベントはファイルの読み込みが成功した・失敗したに限らず発生します。なのでファイルの内容が正常に読み込めた時に発生するloadイベント内で\<img>を作ります。


```javascript:preview.js
const photoOrder = [];
//...........省略
const numberOfPhoto = this.files.length;
//...........省略
  const selectedPhotos = [];
  const promise = new Promise((resolve) => {
    for(let i = 0; i < numberOfPhoto; i++){
      const fileReader = new FileReader();
      fileReader.readAsDataURL(this.files[i]);
      fileReader.addEventListener("load", function(){
        const img = document.createElement('img');
        img.src = this.result;
        img.id = 'photo-' + i;
        selectedPhotos.push(img);
        photoOrder.push(i);
        if(selectedPhotos.length == numberOfPhoto){
          resolve(selectedPhotos);
        }
      });
    }
  });
```
* 空配列selectedPhotosには、作成した\<img>をpushしていく。最後に枚数分作成できていることを確認してresolve();する。resolve();は、「この処理は終わったので次の処理に移れますよ」という宣言。処理の結果を次の処理でも使う場合には、引数にデータを渡す。つまり配列selectedPhotoは、次の処理に\<img>を渡すためだけのもの。
* `fileReader.readAsDataURL(this.files[i]);`の`this.files[i]`はFileオブジェクトのこと。こちらの図を見ると分かりやすいかもしれない。→[①全ての\<img>を作る](#全てのimgを作る)
* \<img>要素を作成する時、id属性の値に変数「i」を使い、'photo-1'などと連番を振る。その連番を空配列photoOrderにpushする。`console.log(photoOrder);`で出力すると、非同期処理の\<img>要素の先着順が明らかになる。これで画像の位置情報が保存できた（\<div>と配列のインデックスは一致するから）。
<img width="400" alt="スクリーンショット 2023-12-01 110244.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118500/a4b56da1-b350-3c09-2779-3c7a00ee503a.png">

【参考】
・[JavaScript　FileAPIについて学ぶ](https://qiita.com/wada1355/items/8028d87d8d2bc6c00e69 "JavaScript　FileAPIについて学ぶ")

### ②枚数分の\<div>を作り、\<img>を先着順に入れていく
①のpromiseオブジェクトに対しthenメソッドを使用します。これで次の処理を開始します。
```javascript:preview.js
const allPhotos = document.getElementById('all-photos');
//...........省略
  promise.then((selectedPhotos) => {
    for(let i = 0; i < selectedPhotos.length; i++){//同期処理
      const img = selectedPhotos[i];
      const div = document.createElement('div');
      div.id = 'div-' + i;
      div.appendChild(img);//<img>を先着順に入れる
      allPhotos.appendChild(div);
```
* ①で作成した配列selectedPhotosから1つずつ\<img>を取り出し、親要素\<div>へ入れていく。
* \<div>と\<img>のセットを、all-photosに追加してプレビュー完成。

CSSで「2つ横に並べたら下に折り返す」ようにしているので、プレビューはこうなります。

<img width="400" alt="2023-11-21 163427.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118500/66a1d6b8-c9a9-e128-9668-283fd76274f4.png">

これで完成です。ここまでで、
* 画像の枚数に合わせたレイアウトで画像をプレビューする。
* `photoOrder = [3, 1, 2, 0];`になっている。
## （4）ドラッグ＆ドロップ
これからやるべきことは、以下3つです。
* [①必要な要素を取得する](#必要な要素を取得する)
* [②画像を入れ替える](#画像を入れ替える)
* [③位置情報を保存](#位置情報を保存)

これらの処理は、ドラッグ＆ドロップ関係のイベントリスナー内に書きます。前回のコードに付け加えます。
```javascript:preview.js
  promise.then((selectedPhotos) => {
    for(let i = 0; i < selectedPhotos.length; i++){
      const img = selectedPhotos[i];
      const div = document.createElement('div');
      div.id = 'div-' + i;
      div.appendChild(img);
      allPhotos.appendChild(div);

      img.addEventListener('dragstart', function(event){
      //処理
      });
      img.addEventListener('dragover', function(event){
      //処理
      });
      img.addEventListener('dragenter', function(event){
      //処理
      });
      img.addEventListener('drop', function(event){
      //処理
      });
      
    }
  });
```
その前に、要素を「ドラッグできるようにする設定」と「ドロップできるようにする設定」が必要です。画像は既定でドラッグできるようになっているので、「ドロップできるようにする設定」のみです。

実際に、この記事内の画像で試してみて下さい。ドラッグの最中、その要素が半透明のイメージとなって現れ、どこかにドロップしようとすると禁止マークが出ます。これは既定で「その場所にドロップできないようになっている」からです。

### ドロップできるようにする設定
dragenterイベントとdragoverイベントが既定でドロップを禁止しています。なので、両方のイベントで`event.preventDefault();`すればその\<img>の上にドロップできるようになります。つまり、以下の「img」とは「ドロップ先の\<img>要素」を指します。

```javascript:preview.js
      img.addEventListener('dragover', function(event){
        event.preventDefault();
      });
      img.addEventListener('dragenter', function(event){
        event.preventDefault();
      });
```
他のイベントにおいても「img」が指すものは異なります。dragstartイベントでの「img」は「ドラッグ中の\<img>要素」を指し、dropイベントでの「img」は「ドロップ先の\<img>要素」を指します。各イベント内で`console.log(img);`をすると分かりやすいです。

【参考】
・[「ドロップ先の指定」](https://developer.mozilla.org/ja/docs/Web/API/HTML_Drag_and_Drop_API/Drag_operations#%E3%83%89%E3%83%A9%E3%83%83%E3%82%B0%E3%81%AE%E5%8A%B9%E6%9E%9C)の項

### ①必要な要素を取得する
ここで例として、ドラッグ中のphoto-3と、ドロップ先のphoto-1を入れ替えることにします。

<img width="300" alt="2023-11-21 233858.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118500/6f510a27-c5f7-37ac-4ae1-0b465dfaa278.png">

「ドロップしたら」画像を入れ替えたいので、dropイベントに書いていきます。ここでは、画像の入れ替えに必要な要素を取得します。
* ドラッグ中の\<img>要素・・・photo-3
* ドロップ先の\<img>要素・・・photo-1【★取得済み】
* ドラッグ中の\<img>要素が入っている\<div>・・・div-0
* ドロップ先の\<img>要素が入っている\<div>・・・div-1

画像の入れ替えは、互いの\<div>に\<img>を追加します。なのであと3つの要素が必要です。

先ほど述べたとおり、dropイベントでの「img」は「ドロップ先の\<img>要素」を指します。なので【★取得済み】ということです。入れ替えに必要な「ドラッグ中の\<img>要素」を取得するには、ここで「ドラッグデータ」を取得する必要があります。

[ドラッグデータ](https://developer.mozilla.org/ja/docs/Web/API/HTML_Drag_and_Drop_API/Drag_operations#%E3%83%89%E3%83%A9%E3%83%83%E3%82%B0%E3%83%87%E3%83%BC%E3%82%BF)とは、
* 「ドラッグした要素の情報」を表すもの。
* 正体は「[DataTransferオブジェクト](https://developer.mozilla.org/ja/docs/Web/API/DataTransfer
)」。
* すべてのドラッグ＆ドロップ関連のイベント内で`event.dataTransfer`に保持される。
* これを使ってドラッグ中のデータに視覚的な設定をしたり、実際にデータを取得する。

データを取得する時は、様々なデータ型で受け取ることができます。どのデータ型で取得できるかはtypesプロパティで確認できます。

```javascript:preview.js
      img.addEventListener('drop', function(event){
        console.log(event.dataTransfer.types);
      });
```
```
(4) ['text/plain', 'text/uri-list', 'text/html', 'Files']
```
データを取得するには、getDataメソッドに取得したいデータの型（text/plainといったMIMEタイプのもの）を渡します。以下は、全て同じデータが取得できます。

【参考】
・[「ドロップの実行」](https://developer.mozilla.org/ja/docs/Web/API/HTML_Drag_and_Drop_API/Drag_operations#%E3%83%89%E3%83%AD%E3%83%83%E3%83%97%E3%81%AE%E5%AE%9F%E8%A1%8C)の項

```javascript:preview.js
      img.addEventListener('drop', function(event){
        event.dataTransfer.getData("text/plain");
        event.dataTransfer.getData("text/uri-list");
        event.dataTransfer.getData("text/html");
      });
```
中身は「ドラッグ中の\<img>のsrc属性の値（データURL）」です。出力すると長いです。

しかし、src属性の値だけでは足りません。単に画像を入れ替えるだけならsrc属性の値を入れ替えるだけで良いのですが、今回は順番通りにアップロードしたいです。そのために位置情報であるphotoOrderを更新しないといけません。それに使っている連番付きのid属性が欲しいです。そこで、**ドラッグデータ（dataTransferオブジェクト）に「ドラッグ中の\<img>のid属性の値」を登録し、dropイベント内で取得できるようにします。**

ドラッグデータに値を登録するには、ドラッグ開始時（dragstartイベント）にsetDataメソッドを使って、「データ型」と「データ」を引数に渡します。

【参考】
・[「ドラッグデータ」](https://developer.mozilla.org/ja/docs/Web/API/HTML_Drag_and_Drop_API/Drag_operations#%E3%83%89%E3%83%AD%E3%83%83%E3%83%97%E3%81%AE%E5%AE%9F%E8%A1%8C)の項

```javascript:preview.js
      img.addEventListener('dragstart', function(event){
        event.dataTransfer.setData("text/plane", img.id);//'photo-3'をドラッグデータに設定
      });
```
先ほど述べたとおり、dragstartイベントでの「img」は「ドラッグ中の\<img>要素」を指します。

dropイベントで、getDataメソッドに型を指定してデータを取得します。そして、取得したid属性から「ドラッグ中の\<img>要素」を取得します。
```javascript:preview.js
      img.addEventListener('drop', function(event){
        const dragImgId = event.dataTransfer.getData("text/plane");//'photo-3'
        const dragImg = document.getElementById(dragImgId);
      });
```
これで2つの\<img>が用意できました。
* dragImg・・・ドラッグ中の\<img>要素（photo-3）
* img・・・ドロップ先の\<img>要素（photo-1）

次に、それぞれの親要素\<div>を取得します。
ここで配列photoOrderです。**\<div>と配列のインデックスは一致します**。

<img width="400" alt="スクリーンショット 2023-12-01 110244.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118500/a4b56da1-b350-3c09-2779-3c7a00ee503a.png">

つまり、
* ドラッグ中のphoto-3が入っている\<div>・・・「3があるインデックス」→0
* ドラッグ先のphoto-1が入っている\<div>・・・「1があるインデックス」→1

を求めます。findIndexメソッドを使い、「配列内の要素で○○があるインデックス」を返します。

```javascript:preview.js
      img.addEventListener('drop', function(event){
        const dragImgId = event.dataTransfer.getData("text/plane");
        const dragImg = document.getElementById(dragImgId);
        function callbackFn(element){
          if(element === this){
            return true;
          }
          return false;
        }
        const dragImgOrder = photoOrder.findIndex(callbackFn, parseInt(dragImg.id.slice(6)));
        const dropImgOrder = photoOrder.findIndex(callbackFn, parseInt(img.id.slice(6)));
      });
``` 
* parseInt関数は、文字列'123'を数値の123に変換する。id属性の値'photo-3'や'photo-1'の数字部分のみをslice関数で抜き出しているが、これは文字列の'3'、'1'だから（配列photoOrderには数値が入っている）。
* callbackFnのelementは配列photoOrderの各要素。1つ1つ調べていく。thisはfindIndex関数の第二引数に渡した値。

返ってきたインデックスを使って\<div>を取得します。

```javascript:preview.js
//.....省略
        const dragImgOrder = photoOrder.findIndex(callbackFn, parseInt(dragImg.id.slice(6)));
        const dropImgOrder = photoOrder.findIndex(callbackFn, parseInt(img.id.slice(6)));
        //ここから
        const dragImgDiv = document.getElementById('div-' + dragImgOrder);
        const dropImgDiv = document.getElementById('div-' + dropImgOrder);
      });
```

これで必要な要素が全て揃いました。
* ドラッグ中の<img>要素・・・photo-3
* ドロップ先の<img>要素・・・photo-1
* ドラッグ中の<img>要素が入っている\<div>・・・div-0
* ドロップ先の<img>要素が入っている\<div>・・・div-1

### ②画像を入れ替える
互いの\<div>に\<img>を追加して入れ替えます。

```javascript:preview.js
//.....省略
        const dragImgDiv = document.getElementById('div-' + dragImgOrder);
        const dropImgDiv = document.getElementById('div-' + dropImgOrder);
        //ここから
        dragImgDiv.appendChild(img);
        dropImgDiv.appendChild(dragImg);
```

### ③位置情報を保存
配列photoOrderを更新します。**\<div>と配列のインデックスは一致する**ので、
* photoOrder[0] = 1;
* photoOrder[1] = 3;

<img width="400" alt="2023-11-21 233319.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118500/a8c5e6af-5cfe-6334-63d0-b3fabb4e9d69.png">

全ての要素に適用できるように書き換えると、

photoOrder **[ドラッグ中の\<img>が入っていた\<div>]** = ドロップ先の\<img>
photoOrder **[ドロップ先の\<img>が入っていた\<div>]** = ドラッグ中の\<img>

```javascript:preview.js
//.....省略
        dragImgDiv.appendChild(img);
        dropImgDiv.appendChild(dragImg);
        //ここから
        photoOrder[dragImgOrder] = parseInt(img.id.slice(6));
        photoOrder[dropImgOrder] = parseInt(dragImg.id.slice(6));
      });
```
これで完成です。
* ドラッグ＆ドロップで画像の入れ替えができる。
* `photoOrder = [1, 3, 2, 0];`になっている。

【参考】
・[JavaScriptでドラッグ＆ドロップによる画像の入れ替えを実装する](https://blog.ver001.com/javascript-dragdrop-image-switch/ "JavaScriptでドラッグ＆ドロップによる画像の入れ替えを実装する")

## （5）JavaScriptで作った位置情報を\<form>で送信できるようにする
* `<input type="hidden" name="photo-order" value="[1, 3, 2, 0]"/>`を作り、\<form>に追加。
* value属性にjavaScriptで生成した値を埋め込むには、JSON形式に変換する。変換しないで試したら、送信先（PHP側）で上手く配列として値が取り出せなかった。

```javascript:preview.js
const upload = document.getElementById('upload');//<form>を取得
//........省略
upload.addEventListener("submit", function(){
  const json = JSON.stringify(photoOrder);// 配列をJSON形式に変換
  const input = document.createElement('input');
  input.type = "hidden";
  input.name = "photo-order";
  input.value = json;
  upload.appendChild(input);
});
```
### preview.js【完成】
```javascript:preview.js
'use strict';
const photo = document.getElementById('photo');
const upload = document.getElementById('upload');
const photoOrder = [];

photo.addEventListener("change", function(){
  const numberOfPhoto = this.files.length;
  const allPhotos = document.getElementById('all-photos');
  allPhotos.classList.add('layout-' + numberOfPhoto);
  const selectedPhotos = [];
  const promise = new Promise((resolve) => {
    for(let i = 0; i < numberOfPhoto; i++){
      const fileReader = new FileReader();
      fileReader.readAsDataURL(this.files[i]);
      fileReader.addEventListener("load", function(){
        const img = document.createElement('img');
        img.src = this.result;
        img.id = 'photo-' + i;
        selectedPhotos.push(img);
        photoOrder.push(i);
        if(selectedPhotos.length == numberOfPhoto){
          resolve(selectedPhotos);
        }
      });
    }
  });
  promise.then((selectedPhotos) => {
    for(let i = 0; i < selectedPhotos.length; i++){
      const img = selectedPhotos[i];
      const div = document.createElement('div');
      div.id = 'div-' + i;
      div.appendChild(img);
      allPhotos.appendChild(div);

      img.addEventListener('dragstart', function(event){
        event.dataTransfer.setData("text/plane", img.id);
      });
      img.addEventListener('dragover', function(event){
        event.preventDefault();
      });
      img.addEventListener('dragenter', function(event){
        event.preventDefault();
      });
      img.addEventListener('drop', function(event){
        const dragImgId = event.dataTransfer.getData("text/plane");
        const dragImg = document.getElementById(dragImgId);
        function callbackFn(element){
          if(element === this){
            return true;
          }
          return false;
        }
        const dragImgOrder = photoOrder.findIndex(callbackFn, parseInt(dragImg.id.slice(6)));
        const dropImgOrder = photoOrder.findIndex(callbackFn, parseInt(img.id.slice(6)));
        const dragImgDiv = document.getElementById('div-' + dragImgOrder);
        const dropImgDiv = document.getElementById('div-' + dropImgOrder);
        dragImgDiv.appendChild(img);
        dropImgDiv.appendChild(dragImg);
        photoOrder[dragImgOrder] = parseInt(img.id.slice(6));
        photoOrder[dropImgOrder] = parseInt(dragImg.id.slice(6));
      });
    }
  });
});
upload.addEventListener("submit", function(){
  const json = JSON.stringify(photoOrder);
  const input = document.createElement('input');
  input.type = "hidden";
  input.name = "photo-order";
  input.value = json;
  upload.appendChild(input);
});
```
これで、**画像と位置情報photoOrder**を送信することができます。送信先のファイルではphotoOrderを使って、プレビューと同じ並びで画像を表示します。

続きはこちらに書きました。

https://qiita.com/kai_163/items/3dea6c5235e7bf9ecf78