blog post cover image

フレームワーク、ライブラリ無しで爆速でドラッグアンドドロップを作成 JS HTML CSS

投稿日付:10/1/2022
1

どうやって実現するか


結論から言うと、drag and dropのWEB APIが用意されているので、実は特殊なアルゴリズムなど考える必要は全くないです。

見た目の作成


html を作成

<div id="drag-and-drop-area">
    <span id="progress"></span>
</div>

#drag-and-drop-areaはドラッグアンドドロップが機能する範囲で、#progressはファイルの読み込みの状態を表す為です。

css作成


/* 基本的なスタイル */
html,
body {
  height: 100%;
  width: 100%;
  padding: 0;
  margin: 0;
}
body {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}
img,
video {
  height: 100%;
  width: 100%;
  position: relative;
  z-index: 2; /* progressの前に表示するため */
}

/* ドラッグアンドドロップが機能する範囲のスタイル  */
#drag-and-drop-area {
  max-width: 80%;
  min-width: 50%;
  min-height: 20%;
  margin: auto;
  border: solid 1px gray;
  padding: 1rem;
  transition: all 0.46s ease;
  position: relative;
}
/* afterを使って中に'Drag and Drop'というテキストを表示 */
#drag-and-drop-area::after {
  content: "Drag and Drop";
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 1;
}

そしたら↓のようになるはずです。 drag-and-drop-example-layout

見た目の部分はできました!

処理の作成


まず必要なDOM要素を取得します


/*DOM要素を取得*/
let drag_and_drop_area = document.querySelector("#drag-and-drop-area");
let progress_status = document.querySelector("#progress");

要素にイベントリスナーを追加


/*DOM要素を取得*/
let drag_and_drop_area = document.querySelector("#drag-and-drop-area");
let progress_status = document.querySelector("#progress");

/* イベントリスナーを追加 */
drag_and_drop_area.addEventListener("dragenter", handleDragIn);
drag_and_drop_area.addEventListener("dragleave", handleDragOut);
drag_and_drop_area.addEventListener("dragover", handleDrag);
drag_and_drop_area.addEventListener("drop", handleDrop);

イベントリスナーのコールバック関数を作成 注意:jsは順番に実行されるのでイベントリスナーの前に関数を定義する必要があります。


/*DOM要素を取得*/
let drag_and_drop_area = document.querySelector("#drag-and-drop-area");
let progress_status = document.querySelector("#progress");

/*ドラッグインの時に呼ばれる*/
const handleDragIn = e => {

};

/*ドラッグアウトの時に呼ばれる*/
const handleDragOut = e => {

};

/*ドラッグしたままの時に呼ばれる*/
const handleDrag = e => {

};

/*ドロップの時に呼ばれる*/
const handleDrop = e => {

};

/* イベントリスナーを追加 */
drag_and_drop_area.addEventListener("dragenter", handleDragIn);
drag_and_drop_area.addEventListener("dragleave", handleDragOut);
drag_and_drop_area.addEventListener("dragover", handleDrag);
drag_and_drop_area.addEventListener("drop", handleDrop);

このままだと、画像をドラッグアンドドロップした時にブラウザのタブで画像が開かれてしまうので、それを制御する処理を追加します。

新規タブが開かないように制御


/*DOM要素を取得*/
let drag_and_drop_area = document.querySelector("#drag-and-drop-area");
let progress_status = document.querySelector("#progress");

/*ドラッグインの時に呼ばれる*/
const handleDragIn = e => {
  /*イベント停止と伝達を無効にする*/  
  e.preventDefault();
  e.stopPropagation();
};

/*ドラッグアウトの時に呼ばれる*/
const handleDragOut = e => {
  /*イベント停止と伝達を無効にする*/  
  e.preventDefault();
  e.stopPropagation();
};

/*ドラッグしたままの時に呼ばれる*/
const handleDrag = e => {
  /*イベント停止と伝達を無効にする*/
  e.preventDefault();
  e.stopPropagation();
};

/*ドロップの時に呼ばれる*/
const handleDrop = e => {
  /*イベント停止と伝達を無効にする*/
  e.preventDefault();  
  e.stopPropagation();
};

/* イベントリスナーを追加 */
drag_and_drop_area.addEventListener("dragenter", handleDragIn);
drag_and_drop_area.addEventListener("dragleave", handleDragOut);
drag_and_drop_area.addEventListener("dragover", handleDrag);
drag_and_drop_area.addEventListener("drop", handleDrop);

ユーザーがファイルをドラッグアンドドロップエリアに持ってきた時に何かの変化が起こるようにしておきます、例えば背景が変わるとか?


/*DOM要素を取得*/
let drag_and_drop_area = document.querySelector("#drag-and-drop-area");
let progress_status = document.querySelector("#progress");

/*ドラッグインの時に呼ばれる*/
const handleDragIn = e => {
  /*イベント停止と伝達を無効にする*/  
  e.preventDefault();
  e.stopPropagation();

  /*エリアに入った時に背景色を変更する*/
  drag_and_drop_area.style.background = "lightblue";
};

/*ドラッグアウトの時に呼ばれる*/
const handleDragOut = e => {
  /*イベント停止と伝達を無効にする*/  
  e.preventDefault();
  e.stopPropagation();

  /*エリアに入った時に背景色を変更する*/
  drag_and_drop_area.style.background = "lightblue";
};

/*ドラッグしたままの時に呼ばれる*/
const handleDrag = e => {
  /*イベント停止と伝達を無効にする*/
  e.preventDefault();
  e.stopPropagation();
};

/*ドロップの時に呼ばれる*/
const handleDrop = e => {
  /*イベント停止と伝達を無効にする*/
  e.preventDefault();  
  e.stopPropagation();

  /*エリアに入った時に背景色を変更する*/
  drag_and_drop_area.style.background = "lightblue";
};

/* イベントリスナーを追加 */
drag_and_drop_area.addEventListener("dragenter", handleDragIn);
drag_and_drop_area.addEventListener("dragleave", handleDragOut);
drag_and_drop_area.addEventListener("dragover", handleDrag);
drag_and_drop_area.addEventListener("drop", handleDrop);

これで↓のようになるはずです

drag in drag in

次はファイルがドロップされた時にファイルを読み込む処理をhandleDrop の中に追加していきます。 ファイルを読み込むにはjsのReadFile APIを使います。 コードが読みやすいようにreadFileという関数を定義して、handleDropの中に読んで上げます。


const readFile = async (blob, progress_status) => {
  const rf = new FileReader();
  rf.readAsDataURL(blob);

  return new Promise((resolve, reject) => {
    rf.onloadstart = () => {
      progress_status.textContent = "読み込み中...";
    };

    rf.onload = () => {
      progress_status.textContent = "読み込みに成功しました";
      resolve(rf.result);
    };

    rf.onerror = () => {
      progress_status.textContent = "エラーが発生しました!";
      reject("エラーが発生しました!");
    };
  });
};

パラメータとしては、file blobとprogressのDOMを受け取り、読み込みが成功か失敗かによって状態変更してあげるような感じですね。

handleDropの中でファイルを読み込む


...

/*** 省略 **/

/*ドロップの時に呼ばれる*/
const handleDrop = e => {
  /*イベント停止と伝達を無効にする*/
  e.preventDefault();
  e.stopPropagation();

  /*ファイルを変数に入れる*/
  const file = e.dataTransfer.files[0];

  /*ファイルを読み込みでBase64で値を取得*/
    readFile(file /*ファイルオブジェクト*/, progress_status /*ステータス要素のDOM*/).then(result => {

        console.log(result);

        /*もし画像だったら*/
        if (file.type === "image/png") {

            /*img要素を生成する*/
            let img = document.createElement("img");

            /*Base64の値を格納する*/
            img.src = result;

            /*imgタグをdrag-and-drop-areaの中に追加する*/
            drag_and_drop_area.appendChild(img);

        }

        /*もし動画だったら*/
        if (file.type === "video/mp4") {

            /*video要素を生成*/
            let video = document.createElement("video");

            /*コントロールボタンを有効*/
            video.setAttribute("controls", true);

            /*Base64の値を格納する*/
            video.src = result;

            /*videoタグをdrag-and-drop-areaの中に格納する*/
            drag_and_drop_area.appendChild(video);

        }

    });

    /*背景色を透明に戻す*/
  drag_and_drop_area.style.background = "transparent";
};

/*** 省略 **/

...

ファイルの受け取りはe.dataTransferでできます、配列で入ってくるので、今回は最初の一番名のファイルだけ読み込みような形で作りました、もし複数のファイルを読み込ませたい場合はforEachで回してあげる感じですね。


  /*ファイルを変数に入れる*/
  const files = e.dataTransfer.files; // <---- ここの変更と

  files.forEach(file => { // <---配列をループで回す

    /*ファイルを読み込みでBase64で値を取得*/
    readFile(file /*ファイルオブジェクト*/, progress_status /*ステータス要素のDOM*/).then(result => {

        console.log(result);

        /*もし画像だったら*/
        if (file.type === "image/png") {

            /*img要素を生成する*/
            let img = document.createElement("img");

            /*Base64の値を格納する*/
            img.src = result;

            /*imgタグをdrag-and-drop-areaの中に追加する*/
            drag_and_drop_area.appendChild(img);

        }

        /*もし動画だったら*/
        if (file.type === "video/mp4") {

            /*video要素を生成*/
            let video = document.createElement("video");

            /*コントロールボタンを有効*/
            video.setAttribute("controls", true);

            /*Base64の値を格納する*/
            video.src = result;

            /*videoタグをdrag-and-drop-areaの中に格納する*/
            drag_and_drop_area.appendChild(video);

        }

    });

  }

今回は画像か動画かでプレビューされるタグを変えていますが、handleDropの中に画像だけ受け取るような処理もかけると思います!

完成コード


html

<div id="drag-and-drop-area">
    <span id="progress"></span>
</div>

css


html,
body {
  height: 100%;
  width: 100%;
  padding: 0;
  margin: 0;
}
body {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}
img,
video {
  height: 100%;
  width: 100%;
  position: relative;
  z-index: 2;
}
#drag-and-drop-area {
  max-width: 80%;
  min-width: 50%;
  min-height: 20%;
  margin: auto;
  border: solid 1px gray;
  padding: 1rem;
  transition: all 0.46s ease;
  position: relative;
}
#drag-and-drop-area::after {
  content: "Drag and Drop";
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 1;
}

javascript


/*DOM要素を取得*/
let drag_and_drop_area = document.querySelector("#drag-and-drop-area");
let progress_status = document.querySelector("#progress");

const readFile = async (blob, progress_status) => {
  const rf = new FileReader();
  rf.readAsDataURL(blob);

  return new Promise((resolve, reject) => {
    rf.onloadstart = () => {
      progress_status.textContent = "読み込み中...";
    };

    rf.onload = () => {
      progress_status.textContent = "読み込みに成功しました";
      resolve(rf.result);
    };

    rf.onerror = () => {
      progress_status.textContent = "エラーが発生しました!";
      reject("エラーが発生しました!");
    };
  });
};

/*ドラッグインの時に呼ばれる*/
const handleDragIn = e => {
  /*イベント停止と伝達を無効にする*/
  e.preventDefault();
  e.stopPropagation();

    /*エリアに入った時に背景色を変更する*/
  drag_and_drop_area.style.background = "lightblue";
};

/*ドラッグアウトの時に呼ばれる*/
const handleDragOut = e => {
  /*イベント停止と伝達を無効にする*/
  e.preventDefault();
  e.stopPropagation();

    /*エリアから出た時に背景色を透明に戻す*/
  drag_and_drop_area.style.background = "transparent";
};

/*ドラッグしたままの時に呼ばれる*/
const handleDrag = e => {
  /*イベント停止と伝達を無効にする*/
  e.preventDefault();
  e.stopPropagation();
};

/*ドロップの時に呼ばれる*/
const handleDrop = e => {
  /*イベント停止と伝達を無効にする*/
  e.preventDefault();
  e.stopPropagation();

  /*ファイルを変数に入れる*/
  const file = e.dataTransfer.files[0];

  /*ファイルを読み込みでBase64で値を取得*/
    readFile(file /*ファイルオブジェクト*/, progress_status /*ステータス要素のDOM*/).then(result => {

        console.log(result);

        /*もし画像だったら*/
        if (file.type === "image/png") {

            /*img要素を生成する*/
            let img = document.createElement("img");

            /*Base64の値を格納する*/
            img.src = result;

            /*imgタグをdrag-and-drop-areaの中に追加する*/
            drag_and_drop_area.appendChild(img);

        }

        /*もし動画だったら*/
        if (file.type === "video/mp4") {

            /*video要素を生成*/
            let video = document.createElement("video");

            /*コントロールボタンを有効*/
            video.setAttribute("controls", true);

            /*Base64の値を格納する*/
            video.src = result;

            /*videoタグをdrag-and-drop-areaの中に格納する*/
            drag_and_drop_area.appendChild(video);

        }

    });

    /*背景色を透明に戻す*/
  drag_and_drop_area.style.background = "transparent";
};

/* イベントリスナーを追加 */
drag_and_drop_area.addEventListener("dragenter", handleDragIn);
drag_and_drop_area.addEventListener("dragleave", handleDragOut);
drag_and_drop_area.addEventListener("dragover", handleDrag);
drag_and_drop_area.addEventListener("drop", handleDrop);

codepen


codepen drag and drop

ではまた!!