Stimulusでドラッグ&ドロップ時に画像のアップロードを行う
はてなフォトっぽいやつを実装してみます。
やり方
view
formのinput file は非表示にしておきます(d-noneスタイル)。
Stimulusのcontrollerは file-drop-controller
というものを用意し、ドラッグ&ドロップの操作はここで行うようにします。
click時の操作は元々用意していた preview-controller
にて実施します。
# haml .uploading-image.pt-3 = form_with url: user_images_path(@user), local: true, data: { controller: "preview file-drop" } do |f| .input-group.d-none = f.file_field :images, class: "form-control override-bs-file image-selection", data: {"action": "input->preview#show", "file-drop-target": "file", "preview-target": "file" } .drag-and-drop-image.mt-2 .drag-and-drop-image-message{"data-action": "click->preview#selectImage dragover->file-drop#dragover dragleave->file-drop#dragleave drop->file-drop#drop drop->preview#show"} クリックしてファイルを選択するか、ここにファイルをドラッグしてください
Stimulus
簡単なclick時の操作から。
上記のようにviewとしては、Stimulusで扱えるようにinput fileにtargetを追加し、clickイベント発生時に selectImage
actionを呼ぶようにしておきます。
Stimulusでは click()
を実行するだけ。
export default class extends Controller { static targets = ["content", "modal", "file"] selectImage() { this.fileTarget.click() } }
続いてドラッグ&ドロップ。
こちらもviewでdragover, dragleave, dropイベントを用意します。
(drop後にはmodalでpreview表示するために、preview#show も実行するようにしています。)
Stimulusではcssで背景を変更するためのstyleの追加・削除を行い、drop時にinput fileへの設定を行なっているだけです。
// app/javascript/controllers/file_drop_controller.js import { Controller } from '@hotwired/stimulus' export default class extends Controller { static targets = ["file"] dragover(e) { e.preventDefault() e.target.classList.add("dragging") } dragleave(e) { e.preventDefault() e.target.classList.remove("dragging") } drop(e) { e.preventDefault() e.target.classList.remove("dragging") const files = e.dataTransfer.files if (typeof files[0] !== 'undefined') { this.fileTarget.files = files } } }
//scss // uploading image .drag-and-drop-image { border: 2px dashed silver; cursor: pointer; font-size: 1.2em; text-align: center; .drag-and-drop-image-message.dragging { background-color: rgb(233, 233, 233); } }
感想
まぁ、Stimlus難しくはないんですよね。単純なやつならいいかなという気はするのですが、もう少しcontrollerとactionが増えてくると辛みがありそうな気がします。
See Also
・cropper.jsを使ってActiveStorageで画像アップロード時に切り抜きを行う
プレビューの実装はこちら
参考
cropper.jsを使ってActiveStorageで画像アップロード時に切り抜きを行う
ググってもあんまり出てこなかったのでメモを残しておきます。
JSは Cropper.js を利用しています。ライセンスはMITです。
こんな感じ
やり方
cropper.jsを使って画像を表示し、保存ボタン押下時に切り抜くための情報をRailsに送ります。 Rails側では保存前に切り抜き処理を行い、保存するだけ。
view
ライブラリ
yarn add で cropperjs を入れます。
file inputを用意します
Stimulusでmodalを表示する設定も入れておきます。
# show.html.haml .uploading-images.pt-3 = form_with url: user_images_path(@user), local: true, data: { controller: "preview" } do |f| .input-group = f.file_field :images, class: "form-control override-bs-file image-selection", "data-action": "input->preview#show"
modalで表示
ポイントはhidden_fieldに cropper.js で取得できる切り抜くための値をセットします。
他にもrotateやscaleも取れます。
#preview-modal.modal{"aria-hidden" => "true", tabindex: "-1", "data-preview-target": "modal"} .modal-dialog .modal-content .modal-header %button.btn-close{"aria-label": "Close", "data-bs-dismiss": "modal", type: "button"} .modal-body .modal-preview-content{ "data-preview-target": "content" } .modal-footer = f.submit "保存", class: "btn btn-outline-dark small modal-submit" = f.hidden_field :image_x = f.hidden_field :image_y = f.hidden_field :image_w = f.hidden_field :image_h
stimlus
modal表示する際に、input fileから画像を読み取りcropper.jsに渡します。
# app/javascript/controllers/preview_controller.js import { Controller } from "@hotwired/stimulus" import * as bootstrap from "bootstrap" import Cropper from "cropperjs" export default class extends Controller { static targets = ["content", "modal"] static bsModal initialize() { this.bsModal = new bootstrap.Modal(this.modalTarget) } show(e) { if (e.target.value !== "") { this.bsModal.show() const file = e.target.files[0] const blob = window.URL.createObjectURL(file) const img = document.createElement("img") img.setAttribute("src", blob) img.classList.add("preview-img") this.contentTarget.innerHTML = '' this.contentTarget.appendChild(img) new Cropper(img, { scalable: false, rotatable: false, zoomable: false, crop(event) { document.getElementById("image_x").value = event.detail.x; document.getElementById("image_y").value = event.detail.y; document.getElementById("image_w").value = event.detail.width; document.getElementById("image_h").value = event.detail.height; } }) } } }
Rails
ライブラリ
画像編集のために、image_processing をインストールします。
かつてはImageMagickが利用されていましたが、今はlibvipsがデフォルトで利用されるとのとこ。
macはbrew install vips でインストールできます。
gem "image_processing", ">= 1.2"
Rails Controller
crop を使います。
crop!
で切り抜きが実行されます。
def create user = User.find(params[:user_id]) uploaded_image = params[:images] cropped_image = ImageProcessing::Vips .source(uploaded_image.tempfile.path) .crop!(params[:image_x].to_f, params[:image_y].to_f, params[:image_w].to_f, params[:image_h].to_f) uploaded_image.tempfile = cropped_image user.images.attach(uploaded_image) redirect_to user_path(user), notice: "画像を登録しました。" end
参考
・Rails,Cropper,CarrierWave,MiniMagickで画像を任意の位置でトリミングして登録するまで - Qiita
RailsのHotwireでCSSアニメーションさせる方法(登録処理もTurbo/Stimlusで実装)
昨日の続きです。
RailsのTurboでAjaxで要素を削除したときにCSSアニメーションさせる方法2(Stimlus化)
画像の登録もTurbo/Stimulusで実施してなかったので修正してみます。
やってみる
css
fade-in用を追加。
.fade-in { animation: fade-in .4s linear; opacity: 1; } .fade-out { animation: fade-out .4s linear; opacity: 0; } @keyframes fade-in { 0% { opacity: 0; } 100% { opacity: 1; } } @keyframes fade-out { 0% { opacity: 1; } 100% { opacity: 0; } }
html
登録用にturbo_frame_tagで囲います。animationを追加したいので、data属性にcontrollerも追加。
# show.html.haml = turbo_frame_tag :user_images, data: { controller: "stream-animations" } do = render partial: "image", collection: @user.images, locals: { user: @user }
Stimulsでアニメーション用のcssを取得できるようにdata属性にleaving_classを追加。
# users/_image.html.haml = turbo_frame_tag "user_image_#{image.id}", data: { entering_class: "fade-in", leaving_class: "fade-out" } do .image-wrapper.position-relative = image_tag image = link_to user_image_path(user, image), remote: true, data: { turbo_method: :delete, turbo_confirm: "Can I delete it?" }, class: "delete-image-link position-absolute top-0 end-0" do %span ×
Railsのcontroller
createでturbo_stream.appendを行います。
class Users::ImagesController < ApplicationController def create user = User.find(params[:user_id]) user.images.attach(params[:images]) image = user.images.last render turbo_stream: turbo_stream.append(:user_images, partial: "users/image", locals: { user:, image: }) end def destroy user = User.find(params[:user_id]) user.images.find(params[:id]).purge render turbo_stream: turbo_stream.remove("user_image_#{params[:id]}") end end
Stimlus
append用の処理を追加。
template, turboFrameElm の辺りが謎ですが、これで追加される要素が取得できます。
なので、これにアニメーション用のクラスを追加すればokです。
# stream_animations_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { connect() { document.addEventListener("turbo:before-stream-render", this.animate) } disconnect() { document.removeEventListener("turbo:before-stream-render", this.animate) } animate(e) { const turboStreamElm = e.target const { action, target } = turboStreamElm if (action === "append") { const template = turboStreamElm.firstElementChild const turboFrameElm = template.content.firstElementChild const { enteringClass } = turboFrameElm.dataset if (enteringClass) { turboFrameElm.classList.add(enteringClass) } } else if (action === "remove") { const turboFrameElm = document.getElementById(target) const { leavingClass } = turboFrameElm.dataset if (leavingClass) { e.preventDefault() turboFrameElm.classList.add(leavingClass) turboFrameElm.addEventListener("animationend", () => { e.target.performAction() }) } } } }
一応できましたが、、、
stimulsの処理自体は大した行数ではないですが、若干面倒に感じます。cssアニメーションはデフォルト機能として欲しいところ。
参考
See Also
・RailsのTurboでAjaxで要素を削除したときにCSSアニメーションさせる方法
・RailsのTurboでAjaxで要素を削除したときにCSSアニメーションさせる方法2(Stimlus化)