プログラミング初心者の勉強ブログ #90
Railsアプリに画像アップロードにトリミング機能を付ける方法です。「carrierwave」と「mini_magick」で実装したアップローダーに、「cropper.js」というjQueryライブラリを使って画像選択後トリミング画面を挟んでいきます。
目次
[toc]
実装内容
Railsアプリの「ユーザー編集画面」でトリミング機能付き画像アップロードを行います。
※jQueryインストール済み且つアップロード機能も実装済みであることを前提として書きます。
※file_fieldのデザイン変更やプレビュー表示については、【画像アップロード】file_fieldデザイン変更+画像選択時にプレビュー表示する方法に書いております。
viewのコーディング
users/edit.html.erb(抜粋)
# 省略 // ajax処理で使う分のパラメータをhidden_fieldで持っておく <%= hidden_field_tag "action", params[:action] %> <%= hidden_field_tag "route", "users" %> <%= hidden_field_tag "idParams", @user.id %> <div class="field image"> <i class="far fa-check-square"></i><%= label 'ユーザーアイコン' %> // 隠したinputタグと紐付けしたdiv <div id="img_field" onClick="$('#upicon').click()" > <% if @user.icon.url.present? %> <%= image_tag @user.icon.url %> <% else %> <i class="fas fa-image"></i><i class="fas fa-file-upload add"></i> <% end %> </div> // inputのfileタグは隠す <%= file_field :icon_http, class: "icon", style: "display:none;", id: "upicon"%> </div> // jsでajax送信するためのボタンを作成 <div class="submit_btn">変更</div> <%= link_to '戻る', root_path, class: "back_btn" %> <div class="delete"> <i class="far fa-trash-alt"></i><%= link_to 'アカウントを削除する', user_path(@user.id), method: :delete, data: {confirm: 'アカウントを削除します。よろしいですか?'}, class: "delete_btn" %> </div> // 画像選択後のトリミング画面用div <div class="overlay" style="display:none;" > <div class="crop_modal"></div> <div class="close_btn"></div> <div class="select_icon_btn">決定</div> </div>
viewでやることは
- 画像アップロード用のfile_fieldの作成
- プレビューを表示させるdivの作成
- トリミング画面となるdivの作成
になるかと思います。今回はfile_fieldのデザインを変更しながら実装しているので、「#img_field」というdivのクリックでfile_fieldが展開し、且つプレビュー表示用のdivとして機能してます。
JavaScript(jQuery)のコーディング
cropper.jsのインストール
cropperjs/dist at master · fengyuanchen/cropperjs
上のGitHubより「cropper.min.js」を「assets/javascripts」下に、
「cropper.min.css」を「assets/stylesheets」下にセットします。
書き込むコード(JavaScript)
$(function(){ // cropper(トリミング部)のコーディング(詳しくはGitHub参照) var cropper; var croppable = false; function initIconCrop(){ cropper = new Cropper(crop_img, { dragMode: 'move', aspectRatio: 1, restore: false, guides: false, center: false, highlight: false, cropBoxMovable: false, cropBoxResizable: false, minCropBoxWidth: 280, minCropBoxHeight: 280, ready: function(){ croppable = true; } }); } // croppedCanvas(トリミング後の画像をプレビューとして表示するための部分)のコーディング var croppedCanvas; function iconCropping(){ if (!croppable) { alert('トリミングする画像が設定されていません。'); return false; } croppedCanvas = cropper.getCroppedCanvas({ width: 280, height: 280, }); var croppedImage = document.createElement('img'); croppedImage.src = croppedCanvas.toDataURL(); img_field.innerHTML = ''; img_field.appendChild(croppedImage); }; // blobへ変換するためのコーディング(blobという形式で画像データを保存するため) var blob; function blobing(){ if (croppedCanvas && croppedCanvas.toBlob){ croppedCanvas.toBlob(function(b){ blob = b; sending(); }); }else if(croppedCanvas && croppedCanvas.msToBlob){ blob = croppedCanvas.msToBlob(); sending(); }else{ blob = null; sending(); }; }; // formデータをまとめてajaxでコントローラーに渡すための準備 function sending(){ var formData = new FormData(); const route = $('#route').val(); const id = $('#idParams').val(); const action = $('#action').val(); // CSRF対策(独自のajax処理を行う場合、head内にあるcsrf-tokenを取得して送る必要がある) $.ajaxPrefilter(function(options, originalOptions, jqXHR){ var token; if (!options.crossDomain){ token = $('meta[name="csrf-token"]').attr('content'); if (token){ return jqXHR.setRequestHeader('X-CSRF-Token', token); }; }; }); // 入力されたformデータをformDataに入れる usersVal(formData); // newとeditでルーティングを区別 if (action == "new"){ $.ajax({ url: '/' + route, datatype: 'json', type: 'post', data: formData, processData: false, contentType: false, }); }else if (action == "edit"){ $.ajax({ url: '/' + route + '/' + id, datatype: 'json', type: 'patch', data: formData, processData: false, contentType: false, }); } }; // 入力されたformデータ(textやradioなど)を取得する関数作成 function usersVal(formData){ name = $('#name').val(); email = $('#email').val(); twitter = $('#twitter').val(); facebook = $('#facebook').val(); content = $('#content').val(); want_to_advertise = $(':radio[name="want_to_advertise"]:checked').val(); want_to_be_advertised = $(':radio[name="want_to_be_advertised"]:checked').val(); if (blob != null){ formData.append('icon', blob); } formData.append('name', name); formData.append('email', email); formData.append('twitter', twitter); formData.append('facebook', facebook); formData.append('content', content); if (want_to_advertise != null){ formData.append('want_to_advertise', want_to_advertise); } if (want_to_be_advertised != null){ formData.append('want_to_be_advertised', want_to_be_advertised); } return formData } // 画像選択時 $('#upicon').on('change', function(e){ file = e.target.files[0]; reader = new FileReader(); if(file.type.indexOf('image') < 0){ return false; }; # トリミング画面をフェードインさせる reader.onload = (function(e){ $('.overlay').fadeIn(); $('.crop_modal').append($('<img>').attr({ src: e.target.result, height: "100%", class: "preview", id: "crop_img", title: file.name })); initIconCrop(); }); reader.readAsDataURL(file); }); // トリミング決定時 $('.select_icon_btn').on('click', function(){ iconCropping(); $('.overlay').fadeOut(); $('#crop_img').remove(); $('.cropper-container').remove(); }); // トリミング画面を閉じる時 $('.close_btn').on('click', function(){ $('.overlay').fadeOut(); $('#crop_img').remove(); $('.cropper-container').remove(); }); // コントローラーへ送信 $('.submit_btn').on('click', function(){ blobing(); }); });
JSでは
- 実際にトリミングを行う要素(cropper.js)の生成
- トリミング後のプレビュー用の画像の取得と表示
- フォームデータの取得
- 画像データのblob形式への変換処理
- コントローラーへパラメーターを送るためのajax処理
を行います。今回はformに入力された値をすべてjsで取得しているためその分冗長になってます。
ポイントは「formData」を用いてパラメーター送信をしている点と、CSRF対策をコーディングしておく点です。CSRF対策は開発環境では必要ありませんが、本番環境はこれが無いとエラーが出ます。
CSRFはそもそもはRailsが勝手にやってくれているもので、自分で勝手にajax通信するときはトークンを所得して送信しなければならないみたいです。
書き込むコード(CSS)
トリミング画面用div
// オーバーレイ .overlay { z-index: 9999; position: fixed; display: none; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.85); } // トリミング部の親要素 .overlay .crop_modal { width: 400px; height: 400px; position: absolute; top: 100px; right: 0; left: 0; margin: 0 auto; } // トリミング部を一部デフォルトからデザイン変更 .overlay .crop_modal .cropper-view-box { outline: none; border: solid 2px #C84289; border-radius: 5px; } // 閉じるボタンを擬似要素で作成 .overlay .close_btn { width: 50px; height: 50px; position: absolute; top: 20px; right: 40px; } .overlay .close_btn:before { display: block; content: ""; width: 1px; height: 36px; background-color: #fff; transform: rotate(45deg); position: absolute; top: 8px; left: 25px; } .overlay .close_btn:after { display: block; content: ""; width: 1px; height: 36px; background-color: #fff; transform: rotate(-45deg); position: absolute; top: 8px; left: 25px; } // トリミング決定ボタン .overlay .select_icon_btn { letter-spacing: normal; width: 200px; height: 40px; line-height: 40px; background-color: #C84289; color: #fff; border-radius: 5px; text-align: center; box-sizing: border-box; position: absolute; top: 530px; right: 0; left: 0; margin: 0 auto; cursor: pointer; }
controllerのコーディング
users_controller.rb(抜粋)
class UsersController < ApplicationController before_action :authenticate_user! # 省略 def edit @user = User.find(params[:id]) redirect_to advertisements_path, alert: "お探しのページは表示できません。" unless @user == current_user end def update // format.jsでリダイレクト先を指定 if current_user.update(set_params) respond_to do |format| flash[:notice] = "ユーザー情報を更新しました。" format.js { render ajax_redirect_to(user_path(current_user.id)) } end else respond_to do |format| flash[:danger] = current_user.errors.full_messages format.js {render 'layouts/error'} end end end def destroy @user = current_user @user.destroy sign_out(@user) redirect_to root_path, notice:"アカウント削除完了" end private def set_params params.permit(:name, :content, :email, :twitter, :facebook, :icon, :icon_cache, :want_to_advertise, :want_to_be_advertised) end def ajax_redirect_to(redirect_uri) { js: "window.location.replace('#{redirect_uri}');" } end end
まとめ
当初はajaxなど使わず普通にlocal trueでform送信していたのですが、トリミング機能を付けるためだけにform送信を大幅に変更することになりました。おそらくもっと簡単に実装できる方法もあるのでは無いかと感じてます。
トリミング機能をつければ、こちらが指定したサイズで画像がすべて保存されるため、レイアウトがやりやすいです。
以上ありがとうございました。