↓↓クリックして頂けると励みになります。
【24 | プロジェクトの登録】 << 【ホーム】 >> 【26 | タスクの登録】
Dropzone.jsをRuby on Railsプロジェクトに統合することは、ファイルアップロードのユーザーエクスペリエンスを向上させ、アプリケーション全体の機能性と使いやすさを向上させるために非常に有益です。
Dropzone.jsは、ユーザーエクスペリエンスを向上させるために、ドラッグアンドドロップを使用してファイルをアップロードする方法を提供します。
ユーザーはファイルを選択するだけでなく、ファイルをブラウザウィンドウにドラッグしてアップロードできます。
これにより、使いやすさが向上し、アップロードプロセスがシームレスになります。
Rails7.1使用の場合、Importmapからの読み込みがうまくいかないため、CDN経由でスクリプトを読み込ませるようにします。
「app/views/layouts/application.html.erb」ファイルのheadタグに以下の記述を追加します。
<!-- Dropzone5.5.1 --> <script src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.5.1/min/dropzone.min.js" integrity="sha512-jytq61HY3/eCNwWirBhRofDxujTCMFEiQeTe+kHR4eYLNTXrUq7kY2qQDKOUnsVAKN5XGBJjQ3TvNkIkW/itGw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
記述追加 【app/views/layouts/application.html.erb】23行目
<!DOCTYPE html> <html> <head> <title>StreamAcademe</title> <meta name="viewport" content="width=device-width,initial-scale=1"> <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> <%= javascript_importmap_tags %> <!-- noty --> <script src="https://cdnjs.cloudflare.com/ajax/libs/noty/3.1.4/noty.min.js" integrity="sha512-lOrm9FgT1LKOJRUXF3tp6QaMorJftUjowOWiDcG5GFZ/q7ukof19V0HKx/GWzXCdt9zYju3/KhBNdCLzK8b90Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <!-- Google Fonts --> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Kaisei+Opti&family=Kosugi+Maru&family=Rampart+One&display=swap" rel="stylesheet"> <!-- Font Awesome --> <script src="https://kit.fontawesome.com/dd8c589546.js" crossorigin="anonymous"></script> <!-- Dropzone5.5.1 --> <script src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.5.1/min/dropzone.min.js" integrity="sha512-jytq61HY3/eCNwWirBhRofDxujTCMFEiQeTe+kHR4eYLNTXrUq7kY2qQDKOUnsVAKN5XGBJjQ3TvNkIkW/itGw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> </head> <body> <!-- ナビゲーションバー --> <%= render "shared/navbar" %> <!-- noty --> <%= render 'shared/notification' %> <%= yield %> </body> </html>
「app/assets/stylesheets」フォルダに「_dropzone.scss」ファイルを新規作成します。
作成した「_dropzone.scss」ファイルを以下のように編集します。
新規作成 【app/assets/stylesheets/_dropzone.scss】
@-webkit-keyframes passing-through{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%, 70%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}100%{opacity:0;-webkit-transform:translateY(-40px);-moz-transform:translateY(-40px);-ms-transform:translateY(-40px);-o-transform:translateY(-40px);transform:translateY(-40px)}}@-moz-keyframes passing-through{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%, 70%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}100%{opacity:0;-webkit-transform:translateY(-40px);-moz-transform:translateY(-40px);-ms-transform:translateY(-40px);-o-transform:translateY(-40px);transform:translateY(-40px)}}@keyframes passing-through{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%, 70%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}100%{opacity:0;-webkit-transform:translateY(-40px);-moz-transform:translateY(-40px);-ms-transform:translateY(-40px);-o-transform:translateY(-40px);transform:translateY(-40px)}}@-webkit-keyframes slide-in{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}}@-moz-keyframes slide-in{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}}@keyframes slide-in{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}}@-webkit-keyframes pulse{0%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}10%{-webkit-transform:scale(1.1);-moz-transform:scale(1.1);-ms-transform:scale(1.1);-o-transform:scale(1.1);transform:scale(1.1)}20%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}}@-moz-keyframes pulse{0%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}10%{-webkit-transform:scale(1.1);-moz-transform:scale(1.1);-ms-transform:scale(1.1);-o-transform:scale(1.1);transform:scale(1.1)}20%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}}@keyframes pulse{0%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}10%{-webkit-transform:scale(1.1);-moz-transform:scale(1.1);-ms-transform:scale(1.1);-o-transform:scale(1.1);transform:scale(1.1)}20%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}}.dropzone,.dropzone *{box-sizing:border-box}.dropzone{min-height:150px;border:2px solid rgba(0,0,0,0.3);background:white;padding:20px 20px}.dropzone.dz-clickable{cursor:pointer}.dropzone.dz-clickable *{cursor:default}.dropzone.dz-clickable .dz-message,.dropzone.dz-clickable .dz-message *{cursor:pointer}.dropzone.dz-started .dz-message{display:none}.dropzone.dz-drag-hover{border-style:solid}.dropzone.dz-drag-hover .dz-message{opacity:0.5}.dropzone .dz-message{text-align:center;margin:2em 0}.dropzone .dz-preview{position:relative;display:inline-block;vertical-align:top;margin:16px;min-height:100px}.dropzone .dz-preview:hover{z-index:1000}.dropzone .dz-preview:hover .dz-details{opacity:1}.dropzone .dz-preview.dz-file-preview .dz-image{border-radius:20px;background:#999;background:linear-gradient(to bottom, #eee, #ddd)}.dropzone .dz-preview.dz-file-preview .dz-details{opacity:1}.dropzone .dz-preview.dz-image-preview{background:white}.dropzone .dz-preview.dz-image-preview .dz-details{-webkit-transition:opacity 0.2s linear;-moz-transition:opacity 0.2s linear;-ms-transition:opacity 0.2s linear;-o-transition:opacity 0.2s linear;transition:opacity 0.2s linear}.dropzone .dz-preview .dz-remove{font-size:14px;text-align:center;display:block;cursor:pointer;border:none}.dropzone .dz-preview .dz-remove:hover{text-decoration:underline}.dropzone .dz-preview:hover .dz-details{opacity:1}.dropzone .dz-preview .dz-details{z-index:20;position:absolute;top:0;left:0;opacity:0;font-size:13px;min-width:100%;max-width:100%;padding:2em 1em;text-align:center;color:rgba(0,0,0,0.9);line-height:150%}.dropzone .dz-preview .dz-details .dz-size{margin-bottom:1em;font-size:16px}.dropzone .dz-preview .dz-details .dz-filename{white-space:nowrap}.dropzone .dz-preview .dz-details .dz-filename:hover span{border:1px solid rgba(200,200,200,0.8);background-color:rgba(255,255,255,0.8)}.dropzone .dz-preview .dz-details .dz-filename:not(:hover){overflow:hidden;text-overflow:ellipsis}.dropzone .dz-preview .dz-details .dz-filename:not(:hover) span{border:1px solid transparent}.dropzone .dz-preview .dz-details .dz-filename span,.dropzone .dz-preview .dz-details .dz-size span{background-color:rgba(255,255,255,0.4);padding:0 0.4em;border-radius:3px}.dropzone .dz-preview:hover .dz-image img{-webkit-transform:scale(1.05, 1.05);-moz-transform:scale(1.05, 1.05);-ms-transform:scale(1.05, 1.05);-o-transform:scale(1.05, 1.05);transform:scale(1.05, 1.05);-webkit-filter:blur(8px);filter:blur(8px)}.dropzone .dz-preview .dz-image{border-radius:20px;overflow:hidden;width:120px;height:120px;position:relative;display:block;z-index:10}.dropzone .dz-preview .dz-image img{display:block}.dropzone .dz-preview.dz-success .dz-success-mark{-webkit-animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);-moz-animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);-ms-animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);-o-animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1)}.dropzone .dz-preview.dz-error .dz-error-mark{opacity:1;-webkit-animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);-moz-animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);-ms-animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);-o-animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1)}.dropzone .dz-preview .dz-success-mark,.dropzone .dz-preview .dz-error-mark{pointer-events:none;opacity:0;z-index:500;position:absolute;display:block;top:50%;left:50%;margin-left:-27px;margin-top:-27px}.dropzone .dz-preview .dz-success-mark svg,.dropzone .dz-preview .dz-error-mark svg{display:block;width:54px;height:54px}.dropzone .dz-preview.dz-processing .dz-progress{opacity:1;-webkit-transition:all 0.2s linear;-moz-transition:all 0.2s linear;-ms-transition:all 0.2s linear;-o-transition:all 0.2s linear;transition:all 0.2s linear}.dropzone .dz-preview.dz-complete .dz-progress{opacity:0;-webkit-transition:opacity 0.4s ease-in;-moz-transition:opacity 0.4s ease-in;-ms-transition:opacity 0.4s ease-in;-o-transition:opacity 0.4s ease-in;transition:opacity 0.4s ease-in}.dropzone .dz-preview:not(.dz-processing) .dz-progress{-webkit-animation:pulse 6s ease infinite;-moz-animation:pulse 6s ease infinite;-ms-animation:pulse 6s ease infinite;-o-animation:pulse 6s ease infinite;animation:pulse 6s ease infinite}.dropzone .dz-preview .dz-progress{opacity:1;z-index:1000;pointer-events:none;position:absolute;height:16px;left:50%;top:50%;margin-top:-8px;width:80px;margin-left:-40px;background:rgba(255,255,255,0.9);-webkit-transform:scale(1);border-radius:8px;overflow:hidden}.dropzone .dz-preview .dz-progress .dz-upload{background:#333;background:linear-gradient(to bottom, #666, #444);position:absolute;top:0;left:0;bottom:0;width:0;-webkit-transition:width 300ms ease-in-out;-moz-transition:width 300ms ease-in-out;-ms-transition:width 300ms ease-in-out;-o-transition:width 300ms ease-in-out;transition:width 300ms ease-in-out}.dropzone .dz-preview.dz-error .dz-error-message{display:block}.dropzone .dz-preview.dz-error:hover .dz-error-message{opacity:1;pointer-events:auto}.dropzone .dz-preview .dz-error-message{pointer-events:none;z-index:1000;position:absolute;display:block;display:none;opacity:0;-webkit-transition:opacity 0.3s ease;-moz-transition:opacity 0.3s ease;-ms-transition:opacity 0.3s ease;-o-transition:opacity 0.3s ease;transition:opacity 0.3s ease;border-radius:8px;font-size:13px;top:130px;left:-10px;width:140px;background:#be2626;background:linear-gradient(to bottom, #be2626, #a92222);padding:0.5em 1.2em;color:white}.dropzone .dz-preview .dz-error-message:after{content:'';position:absolute;top:-6px;left:64px;width:0;height:0;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #be2626}
「app/assets/stylesheets/application.scss」ファイルの21行目に以下の記述を追加します。
@use "./dropzone";
記述追加 【app/assets/stylesheets/application.scss】21行目
/* * This is a manifest file that'll be compiled into application.css, which will include all the files * listed below. * * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's * vendor/assets/stylesheets directory can be referenced here using a relative path. * * You're free to add application-wide styles to this file and they'll appear at the bottom of the * compiled file so the styles you add here take precedence over styles defined in any other CSS * files in this directory. Styles in this file should be added after the last require_* statement. * It is generally better to create a new file per style scope. * *= require_tree . *= require_self */ @import "bootstrap"; @use "./noty"; @use "./dropzone"; body * { font-family: Kosugi Maru; } .font1 { font-family: Rampart One; } .font2 { font-family: Kaisei Opti; } //アバター オンライン .avatar { position: relative; display: inline-block; &::before { content: ""; position: absolute; bottom: 1px; left: 38px; width: 10px; height: 10px; border-radius: 100%; border: 1px solid white; } &.online:before { background-color: #1dbf73; } &.offline:before { background-color: gray; } }
「app/controllers/projects_controller.rb」ファイルを以下の手順で編集していきます。
1.3行目に以下の記述を追加します。
protect_from_forgery except: [:upload_photo]
2.6行目に以下の記述を追加します。
before_action :is_authorised, only: [:naming, :pricing, :description, :photo_upload, :update]
3.27行目に以下の記述を追加します。
new_params = project_params.merge(active: true) if is_ready_project
4.50行目に「upload_photo()」メソッド、「delete_photo()」メソッドを追加します。
def upload_photo @project.images.attach(params[:file]) render json: { success: true } end def delete_photo @image = ActiveStorage::Attachment.find(params[:photo_id]) @image.purge redirect_to photo_upload_project_path(@project) end
5.70行目に以下の記述を追加します。
def is_authorised redirect_to root_path, alert: "権限がありません。" unless current_user.id == @project.user_id end def is_ready_project !@project.active && !@project.price.blank? && !@project.name.blank? && !@project.images.blank? && !@project.description.blank? end
記述更新 app/controllers/projects_controller.rb
class ProjectsController < ApplicationController protect_from_forgery except: [:upload_photo] before_action :authenticate_user!, except: [:show] before_action :set_project, except: [:new, :create, :show, :index] before_action :is_authorised, only: [:naming, :pricing, :description, :photo_upload, :update] def index @projects = Project.all end def new @project = current_user.projects.build end def create @project = current_user.projects.build(project_params) if @project.save redirect_to naming_project_path(@project), notice: "保存しました" else redirect_to request.referrer, flash: { error: @project.errors.full_messages } end end def update new_params = project_params new_params = project_params.merge(active: true) if is_ready_project if @project.update(new_params) flash[:notice] = "保存しました。" else flash[:alert] = { error: @project.errors.full_messages } end redirect_back(fallback_location: request.referer) end def show @project = Project.find(params[:id]) @tasks = @project.tasks.order(:tag) @i = 0 @images = @project.images end def edit @project = Project.find(params[:id]) @tasks = @project.tasks.order(:tag) end def upload_photo @project.images.attach(params[:file]) render json: { success: true } end def delete_photo @image = ActiveStorage::Attachment.find(params[:photo_id]) @image.purge redirect_to photo_upload_project_path(@project) end private def set_project @project = Project.find(params[:id]) end def project_params params.require(:project).permit(:name, :content, :price, :description, :images, :active) end def is_authorised redirect_to root_path, alert: "権限がありません。" unless current_user.id == @project.user_id end def is_ready_project !@project.active && !@project.price.blank? && !@project.name.blank? && !@project.images.blank? && !@project.description.blank? end end
ルートの設定を以下の内容に更新します。
記述更新 config\routes.rb(26行目)
delete :delete_photo post :upload_photo
config\routes.rb
Rails.application.routes.draw do # ルートを app\views\pages\home.html.erb に設定 root 'pages#home' # get get 'pages/home' get '/dashboard', to: 'users#dashboard' get '/users/:id', to: 'users#show', as: 'user' # post post '/users/edit', to: 'users#update' # device devise_for :users, path: '', path_names: {sign_up: 'register', sign_in: 'login', edit: 'profile', sign_out: 'logout'}, controllers: {omniauth_callbacks: 'omniauth_callbacks', registrations: 'registrations'} resources :projects do member do get 'naming' get 'pricing' get 'description' get 'photo_upload' delete :delete_photo post :upload_photo end resources :tasks, only: [:show, :index] end resources :tasks, except: [:edit] do member do get 'naming' get 'description' get 'video' get 'code' end end # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. # Can be used by load balancers and uptime monitors to verify that the app is live. get "up" => "rails/health#show", as: :rails_health_check # Defines the root path route ("/") # root "posts#index" end
「app/views/projects/_project_menu.html.erb」ファイルの22〜26行目を以下のように編集します。
Rails6.1の時は画面推移の時にJavascriptがうまく読み込まれないターボリンクの問題を解決するために「data: { turbolinks: false} 」という記述を入れていましたが、Rails7.1ではturbolinksがturboに置き換わったため、「data: { turbo: false }」という記述に変更しています。
<li class="list-group-item" style="border:none;"> <%= link_to "写真アップロード", photo_upload_project_path, class: "btn btn-light", data: { turbo: false } %> <% if !@project.images.blank? %> <span class="text-danger"><i class="fa fa-check"></i></span> <% end %> </li>
30行目の記述も以下のように変更します。
<% is_ready = !@project.active && !@project.price.blank? && !@project.name.blank? && !@project.description.blank? && !@project.images.blank? %>
app/views/projects/_project_menu.html.erb
<div class="card"> <div class="card-body"> <ul class="list-group"> <li class="list-group-item" style="border:none;"> <%= link_to "タイトル", naming_project_path, class: "btn btn-light" %> <% if !@project.name.blank? %> <span class="text-danger"><i class="fa fa-check"></i></span> <% end %> </li> <li class="list-group-item" style="border:none;"> <%= link_to "内容", description_project_path, class: "btn btn-light" %> <% if !@project.description.blank? %> <span class="text-danger"><i class="fa fa-check"></i></span> <% end %> </li> <li class="list-group-item" style="border:none;"> <%= link_to "価格", pricing_project_path, class: "btn btn-light" %> <% if !@project.price.blank? %> <span class="text-danger"><i class="fa fa-check"></i></span> <% end %> </li> <li class="list-group-item" style="border:none;"> <%= link_to "写真アップロード", photo_upload_project_path, class: "btn btn-light", data: { turbo: false } %> <% if !@project.images.blank? %> <span class="text-danger"><i class="fa fa-check"></i></span> <% end %> </li> </ul> <div class="mt-4"> <% is_ready = !@project.active && !@project.price.blank? && !@project.name.blank? && !@project.description.blank? && !@project.images.blank? %> <%= form_for @project do |f| %> <%= f.hidden_field :active, value: true %> <%= f.submit "公開する", class: "btn btn-danger w-100", disabled: !is_ready %> <% end %> </div> </div> </div>
「app/views/projects/photo_upload.html.erb」ファイルに記述を追加します。
削除のリンクを機能させるにはlink_toメソッドではなく、button_toメソッドを使用する必要があります。
app/views/rooms/photo_upload.html.erb
<div class="container mt-4"> <div class="row"> <div class="col-md-3"> <%= render 'project_menu' %> </div> <div class="col-md-9"> <div class="card"> <div class="card-body"> <h4 class="mt-4 mb-4"><b>写真アップロード</b></h4> <!-- 写真アップロード --> <div class="dropzone" id="myDropzone" style="height: 200px; border: dashed 1px #333; border-radius: 10px; text-align: center; padding-top: 1rem;" action="/projects/<%= @project.id %>/upload_photo"></div> <div class="container"> <div class="row"> <% @project.images.each do |photo| %> <div class="col-4"> <div class="card mt-2"> <%= image_tag url_for(photo), class: "card-img-top" %> <div class="card-body"> <p class="card-text"> <%= button_to '削除', delete_photo_project_url(photo_id: photo.id, id: @project.id), method: :delete, data: { turbo: false }, form: { onSubmit: "return check()" }, style: "z-index: 100;", class: "btn btn-outline-danger btn-sm" %> </p> </div> </div> </div> <% end %> </div> </div> </div> </div> </div> </div> </div> <script> Dropzone.options.myDropzone = { paramName: "file", maxFilesize: 5, acceptedFiles: "image/*", dictDefaultMessage: "ここに写真をドラッグ&ドロップして下さい。<br/>または、クリックすることで写真を選択することもできます。", init: function() { this.on('complete', function (file) { location.reload(); }) } } function check(){ if(window.confirm('本当に削除してよろしいですか?')){ return true; } else{ window.alert('キャンセルされました'); return false; } } </script>
これで写真のアップロード、削除ができます。
複数の写真も同時にアップロードできます。
ブラウザ確認
http://localhost:3000/projects/4/photo_upload
【24 | プロジェクトの登録】 << 【ホーム】 >> 【26 | タスクの登録】
↓↓クリックして頂けると励みになります。