lib/active_storage_drag_and_drop/form_builder.rb
# frozen_string_literal: true
module ActiveStorageDragAndDrop
# Custom FormBuilder module. All code in this module is executed within the context of
# ActionView::Helpers::FormBuilder when ActionView is first loaded via the
# {Engine Engine}
# @since 0.1.0
module FormBuilder
delegate :capture, :content_tag, :tag, :safe_join, to: :@template
# Returns a file upload input tag wrapped in markup that allows dragging and dropping of files
# onto the element.
#
# @author Ian Grant
# @see file:README.md#Usage Usage section of the README
#
# @param [Symbol] method The attribute on the target model to attach the files to.
# @param [String] content The content to render inside of the drag and drop file field.
# @param [Hash] options A hash of options to customise the file field.
#
# @option options [Boolean] :disabled If set to true, the user will not be able to use this
# input.
# @option options [Boolean] :mutiple If set to true, *in most updated browsers* the user will
# be allowed to select multiple files.
# @option options [String] :accept If set to one or multiple mime-types, the user will be
# suggested a filter when choosing a file. You still need to set up model validations.
# @option options [Integer] :size_limit The upper limit on filesize to accept in bytes.
# Client-side validation only. You still need to set up model validations.
#
# @return [String] The generated file field markup.
#
# @example
# # Accept only PNGs or JPEGs up to 5MB in size:
# form.drag_and_drop_file_field :images, nil, accept: 'image/png, image/jpeg',
# size_limit: 5_000_000
# @example
# # Pass custom content string:
# form.drag_and_drop_file_field :images, '<div>Drag and Drop!</div>', accept: 'image/png'
# @example
# # Pass a block of content instead of passing a string
# <%= form.drag_and_drop_file_field(:images, accept: 'image/png') do %>
# <strong>Drag and Drop</strong> PNG files here or <strong>click to browse</strong>
# <% end %>
def drag_and_drop_file_field(method, content_or_options = nil, options = {}, &block)
if block_given?
options = content_or_options if content_or_options.is_a? Hash
drag_and_drop_file_field_string(method, capture(&block), options)
else
drag_and_drop_file_field_string(method, content_or_options, options)
end
end
private
# After {#drag_and_drop_file_field} has parsed whether the content was passed as a block or a
# parameter the result is passed to this method which actually generates the markup.
#
# @author Ian Grant
# @see #drag_and_drop_file_field
#
# @param (see #drag_and_drop_file_field)
# @option (see #drag_and_drop_file_field)
# @return (see #drag_and_drop_file_field)
def drag_and_drop_file_field_string(method, content = nil, param_options = {})
ref = "#{object_name}_#{method}"
options = file_field_options(method, param_options)
content ||= default_content
content = [content]
content << tag.div(id: "asdndz-#{ref}__icon-container", class: 'asdndz__icon-container')
content << file_field(method, options)
content += unpersisted_attachment_fields(method, options[:multiple])
content_tag :label, safe_join(content), class: 'asdndzone', id: "asdndz-#{ref}",
'data-dnd-input-id': ref
end
# returns an array of tags used to pre-populate the the dropzone with tags queueing unpersisted
# file attachments for attachment at the next form submission.
#
# @author Ian Grant
# @param [Symbol] method The attribute on the target model to attach the files to.
# @param [Boolean] multiple Whether the dropzone should accept multiple attachments or not.
# @return [Array] An array of hidden field tags for each unpersisted file attachment.
def unpersisted_attachment_fields(method, multiple)
unpersisted_attachments(method).map.with_index do |attachment, idx|
hidden_field method,
mutiple: multiple ? :multiple : false, value: attachment.signed_id,
name: "#{object_name}[#{method}]#{'[]' if multiple}",
data: {
direct_upload_id: idx,
uploaded_file: { name: attachment.filename, size: attachment.byte_size },
icon_container_id: "asdndz-#{object_name}_#{method}__icon-container"
}
end
end
# Returns an array of all unpersisted file attachments (e.g. left over after a failed
# validation)
#
# @author Ian Grant
# @param [Symbol] method The attribute on the target model to attach the files to.
# @return [Array] An array of unpersisted file attachments.
def unpersisted_attachments(method)
as_relation = @object.send(method)
if as_relation.is_a?(ActiveStorage::Attached::One) && as_relation.attachment.present? &&
!@object.persisted?
[as_relation.attachment]
elsif as_relation.is_a?(ActiveStorage::Attached::Many)
as_relation.reject(&:persisted?)
else
[]
end
end
# Generates a hash of default options for the embedded file input field.
#
# @author Ian Grant
# @param [Symbol] method The attribute on the target model to attach the files to.
# @return [Hash] The default options for the file field
def default_file_field_options(method)
{
multiple: @object.send(method).is_a?(ActiveStorage::Attached::Many),
direct_upload: true,
style: 'opacity: 0;',
data: {
dnd: true,
dnd_zone_id: "asdndz-#{object_name}_#{method}",
icon_container_id: "asdndz-#{object_name}_#{method}__icon-container"
}
}
end
# The default content to populate the drag and drop zone with if there is no content supplied
# in the parameters or passed as a block.
#
# @author Ian Grant
# @since 1.0.0
# @return [String] "Drag & Drop or <span class='asdndz-highlight'>Browse</span>"
def default_content
safe_join(['Drag & Drop or ', tag.span('Browse', class: 'asdndz-highlight')])
end
# Merges the user provided options with the default options overwriting the defaults to
# generate the final options passed to the embedded file input field.
#
# @author Ian Grant
# @param [Symbol] method The attribute on the target model to attach the files to.
# @param [Hash] custom_options The user provided custom options hash.
# @return [Hash] The user provided options and default options merged.
def file_field_options(method, custom_options)
default_file_field_options(method).merge(custom_options) do |_key, default, custom|
default.is_a?(Hash) && custom.is_a?(Hash) ? default.merge(custom) : custom
end
end
end
end