formstrap 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -1
  3. data/app/assets/javascripts/formstrap/controllers/file_preview_controller.js +5 -5
  4. data/app/assets/javascripts/formstrap/controllers/media_controller.js +4 -2
  5. data/app/assets/javascripts/formstrap/controllers/media_modal_controller.js +38 -2
  6. data/app/assets/javascripts/formstrap/controllers/popup_controller.js +75 -0
  7. data/app/assets/javascripts/formstrap/controllers/repeater_controller.js +4 -4
  8. data/app/assets/javascripts/formstrap/index.js +2 -0
  9. data/app/assets/javascripts/formstrap.js +1790 -129
  10. data/app/assets/stylesheets/formstrap/{forms/autocomplete.scss → autocomplete.scss} +1 -1
  11. data/app/assets/stylesheets/formstrap/{forms/file.scss → file.scss} +10 -10
  12. data/app/assets/stylesheets/formstrap/general.scss +8 -1
  13. data/app/assets/stylesheets/formstrap/media/_modal.scss +9 -0
  14. data/app/assets/stylesheets/formstrap/{forms/media.scss → media/_validation.scss} +2 -2
  15. data/app/assets/stylesheets/formstrap/media.scss +2 -1
  16. data/app/assets/stylesheets/formstrap/{forms/repeater.scss → repeater.scss} +8 -8
  17. data/app/assets/stylesheets/formstrap/shared/popup.scss +17 -0
  18. data/app/assets/stylesheets/formstrap/shared/thumbnail.scss +23 -0
  19. data/app/assets/stylesheets/formstrap/shared.scss +2 -0
  20. data/app/assets/stylesheets/formstrap/utilities/dropzone.scss +2 -2
  21. data/app/assets/stylesheets/formstrap/utilities.scss +0 -1
  22. data/app/assets/stylesheets/formstrap.css +155 -143
  23. data/app/assets/stylesheets/formstrap.scss +5 -1
  24. data/app/controllers/formstrap/media_controller.rb +41 -23
  25. data/app/helpers/application_helper.rb +20 -0
  26. data/app/models/concerns/formstrap/attachment.rb +16 -0
  27. data/app/models/concerns/formstrap/blob.rb +56 -0
  28. data/app/models/formstrap/file_view.rb +3 -3
  29. data/app/models/formstrap/media_view.rb +12 -1
  30. data/app/models/formstrap/thumbnail_view.rb +108 -0
  31. data/app/views/formstrap/_autocomplete.html.erb +1 -1
  32. data/app/views/formstrap/_file.html.erb +8 -8
  33. data/app/views/formstrap/_media.html.erb +6 -4
  34. data/app/views/formstrap/_repeater.html.erb +3 -3
  35. data/app/views/formstrap/media/_item.html.erb +10 -7
  36. data/app/views/formstrap/media/_modal.html.erb +35 -8
  37. data/app/views/formstrap/media/_thumbnail.html.erb +12 -8
  38. data/app/views/formstrap/media/_validation.html.erb +1 -1
  39. data/app/views/formstrap/media/create.turbo_stream.erb +3 -1
  40. data/app/views/formstrap/media/index.html.erb +1 -1
  41. data/app/views/formstrap/repeater/_row.html.erb +4 -4
  42. data/app/views/formstrap/shared/_popup.html.erb +1 -1
  43. data/app/views/formstrap/shared/_thumbnail.html.erb +5 -3
  44. data/config/locales/formstrap/forms/en.yml +0 -14
  45. data/config/locales/formstrap/forms/nl.yml +1 -15
  46. data/config/locales/formstrap/media/en.yml +2 -10
  47. data/config/locales/formstrap/media/nl.yml +2 -10
  48. data/config/routes.rb +0 -2
  49. data/lib/formstrap/version.rb +1 -1
  50. metadata +17 -30
  51. data/app/assets/stylesheets/formstrap/forms.scss +0 -12
  52. data/app/assets/stylesheets/formstrap/media/index.scss +0 -9
  53. data/app/assets/stylesheets/formstrap/utilities/buttons.scss +0 -27
  54. data/app/models/formstrap/blocks_view.rb +0 -43
  55. data/app/views/formstrap/_blocks.html.erb +0 -45
  56. data/app/views/formstrap/_to_ary.html.erb +0 -0
  57. data/app/views/formstrap/blocks/_modal.html.erb +0 -20
  58. data/app/views/formstrap/fields/_base.html.erb +0 -25
  59. data/app/views/formstrap/fields/_file.html.erb +0 -17
  60. data/app/views/formstrap/fields/_files.html.erb +0 -17
  61. data/app/views/formstrap/fields/_group.html.erb +0 -52
  62. data/app/views/formstrap/fields/_list.html.erb +0 -31
  63. data/app/views/formstrap/fields/_text.html.erb +0 -17
  64. data/app/views/formstrap/media/_media_item_modal.html.erb +0 -77
  65. data/app/views/formstrap/media/show.html.erb +0 -9
  66. data/app/views/formstrap/media/update.turbo_stream.erb +0 -3
  67. data/config/locales/activerecord/en.yml +0 -12
  68. data/config/locales/activerecord/nl.yml +0 -13
  69. data/config/locales/defaults/en.yml +0 -215
  70. data/config/locales/defaults/nl.yml +0 -213
  71. data/config/locales/devise/en.yml +0 -65
  72. data/config/locales/devise/nl.yml +0 -85
  73. /data/app/assets/stylesheets/formstrap/{forms/search.scss → search.scss} +0 -0
@@ -3,14 +3,11 @@ class Formstrap::MediaController < FormstrapController
3
3
  layout false
4
4
 
5
5
  def index
6
- @blobs =
7
- ActiveStorage::Blob
8
- .not_attached_to_variant
9
- .by_mimetypes_string(media_params[:mimetype])
10
- .order(created_at: :desc)
11
- .group(:id)
12
- .all
13
- @blobs = paginate(@blobs)
6
+ blobs = ActiveStorage::Blob
7
+ blobs = filter(blobs)
8
+ blobs = sort(blobs)
9
+ blobs = blobs.group(:id)
10
+ @blobs = paginate(blobs)
14
11
  @mimetypes = media_params[:mimetype]
15
12
 
16
13
  respond_to do |format|
@@ -37,32 +34,53 @@ class Formstrap::MediaController < FormstrapController
37
34
  @blob = ActiveStorage::Blob.find(params[:id])
38
35
  end
39
36
 
40
- def update
41
- @blob = ActiveStorage::Blob.find(params[:id])
42
- media_item_params[:filename] = media_item_params[:filename] + "." + @blob.filename.to_s.rpartition(".").last
43
- if @blob.update(media_item_params)
44
- flash.now[:notice] = t("admin.flash.updated", name: @blob.filename)
45
- end
46
- end
47
-
48
37
  def thumbnail
49
38
  @blob = ActiveStorage::Blob.find(params[:id])
50
39
  end
51
40
 
52
41
  private
53
42
 
43
+ def filter(blobs)
44
+ blobs = filter_unattached(blobs)
45
+ blobs = filter_by_mimetype(blobs, media_params[:mimetype]) if media_params[:mimetype].present?
46
+ blobs = filter_excluded_models(blobs, media_params[:exclude_models]) if media_params[:exclude_models].present?
47
+ blobs = filter_search(blobs, media_params[:search]) if media_params[:search].present?
48
+ blobs
49
+ end
50
+
51
+ def filter_unattached(blobs)
52
+ blobs.not_attached_to_variant
53
+ end
54
+
55
+ def filter_by_mimetype(blobs, mimetype)
56
+ blobs.by_mimetypes_string(mimetype)
57
+ end
58
+
59
+ def filter_excluded_models(blobs, model_names = [])
60
+ blobs.not_attached_to(model_names)
61
+ end
62
+
63
+ def filter_search(blobs, string)
64
+ blobs.search(string)
65
+ end
66
+
67
+ def sort(blobs)
68
+ blobs.order(created_at: :desc)
69
+ end
70
+
54
71
  def media_params
55
72
  params.permit(
56
- :min,
57
73
  :max,
58
- :name,
59
74
  :mimetype,
75
+ :min,
76
+ :name,
77
+ :search,
78
+ :page,
79
+ :page_start,
80
+ :per_page,
60
81
  ids: [],
61
- files: []
82
+ files: [],
83
+ exclude_models: []
62
84
  )
63
85
  end
64
-
65
- def media_item_params
66
- params.require(:blob).permit!
67
- end
68
86
  end
@@ -0,0 +1,20 @@
1
+ class ApplicationHelper
2
+ def show_svg(attachment, options = {})
3
+ return if !attachment.content_type.include?('svg')
4
+
5
+ attachment.open do |file|
6
+ content = file.read
7
+ doc = Nokogiri::HTML::DocumentFragment.parse content
8
+ svg = doc.at_css 'svg'
9
+
10
+ # for security
11
+ doc.search('script').each do |src|
12
+ src.remove
13
+ end
14
+
15
+ options.each { |attr, value| svg[attr.to_s] = value }
16
+
17
+ doc.to_html.html_safe
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,16 @@
1
+ module Formstrap
2
+ module Attachment
3
+ extend ActiveSupport::Concern
4
+ included do
5
+ scope :not_a_variant, -> { where.not(record_type: "ActiveStorage::VariantRecord") }
6
+
7
+ def root_record
8
+ record
9
+ end
10
+
11
+ def root_record_name
12
+ "#{record.class.name} #{record.id}"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,56 @@
1
+ module Formstrap
2
+ module Blob
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class << self
7
+ def search(query)
8
+ where("active_storage_blobs.filename LIKE ?", "%#{query}%")
9
+ end
10
+
11
+ def not_attached_to_variant
12
+ not_attached_to("ActiveStorage::VariantRecord")
13
+ end
14
+
15
+ def not_attached_to(arg)
16
+ case arg
17
+ when String
18
+ record_types = [arg]
19
+ when Class
20
+ record_types = [arg.to_s]
21
+ when Array
22
+ record_types = arg.map(&:to_s)
23
+ end
24
+
25
+ left_outer_joins(:attachments)
26
+ .where.not(active_storage_attachments: {record_type: record_types})
27
+ .or(is_orphan)
28
+ end
29
+
30
+ def by_mimetypes_string(mimetype_string)
31
+ return where({}) if mimetype_string.blank?
32
+ by_mimetypes(mimetype_string.split(","))
33
+ end
34
+
35
+ private
36
+
37
+ def is_orphan
38
+ left_outer_joins(:attachments)
39
+ .where(active_storage_attachments: {id: nil})
40
+ end
41
+
42
+ def by_mimetypes(mimetypes = [])
43
+ results = self
44
+
45
+ mimetypes.map.with_index do |mimetype, index|
46
+ content_type = mimetype.tr("*", "%")
47
+ query = where(arel_table[:content_type].matches(content_type))
48
+ results = (index == 0) ? query : results.or(query)
49
+ end
50
+
51
+ results
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -23,7 +23,7 @@ module Formstrap
23
23
 
24
24
  def wrapper_options
25
25
  default_wrapper_options.deep_merge({
26
- class: ["mb-3 h-form-file", ("form-floating" if float)],
26
+ class: ["mb-3 formstrap-file", ("form-floating" if float)],
27
27
  data: {
28
28
  controller: ("file-preview" if preview)
29
29
  }
@@ -57,7 +57,7 @@ module Formstrap
57
57
  def dropzone_options
58
58
  if dropzone
59
59
  {
60
- class: ["h-dropzone", validation_class],
60
+ class: ["formstrap-dropzone", validation_class],
61
61
  data: {
62
62
  controller: "dropzone"
63
63
  }
@@ -73,7 +73,7 @@ module Formstrap
73
73
  # {
74
74
  # prepend: prepend,
75
75
  # append: append,
76
- # class: "h-form-file",
76
+ # class: "formstrap-file",
77
77
  # data: {
78
78
  # controller: "#{"file-preview" if preview} #{"dropzone" if dropzone}"
79
79
  # }
@@ -30,7 +30,8 @@ module Formstrap
30
30
 
31
31
  def item_options
32
32
  options = {
33
- sort: sort
33
+ sort: sort,
34
+ url: modal_url,
34
35
  }
35
36
 
36
37
  # Don't pass width or height if it was not defined
@@ -122,6 +123,16 @@ module Formstrap
122
123
  end
123
124
  end
124
125
 
126
+ def modal_url
127
+ formstrap_media_path(name: name, ids: blob_ids, min: min, max: max, mimetype: accept, exclude_models: exclude_models)
128
+ end
129
+
130
+ def edit_modal_url(attachment)
131
+ return nil unless edit_url.present?
132
+ return edit_url unless attachment&.persisted?
133
+ edit_url.gsub(":id", attachment.id.to_s)
134
+ end
135
+
125
136
  attr_reader :accept
126
137
 
127
138
  def required
@@ -0,0 +1,108 @@
1
+ module Formstrap
2
+ class ThumbnailView < ViewModel
3
+ def class_names
4
+ class_names = [@class]
5
+ class_names << "img-thumbnail formstrap-thumbnail"
6
+ class_names.join(" ")
7
+ end
8
+
9
+ def width
10
+ if is_defined?(:width)
11
+ @width
12
+ else
13
+ @width || 100
14
+ end
15
+ end
16
+
17
+ def height
18
+ if is_defined?(:height)
19
+ @height
20
+ else
21
+ @height || 100
22
+ end
23
+ end
24
+
25
+ def blob
26
+ file.is_a?(ActiveStorage::Blob) ? file : file&.blob
27
+ end
28
+
29
+ def mime_type
30
+ blob&.content_type
31
+ end
32
+
33
+ def variant_options
34
+ if width.nil? || height.nil?
35
+ default_variant_options.merge({
36
+ resize_to_fit: [width, height]
37
+ })
38
+ else
39
+ default_variant_options.merge({
40
+ resize_to_fill: [width, height, crop: :attention]
41
+ })
42
+ end
43
+ end
44
+
45
+ def default_variant_options
46
+ {
47
+ saver: {
48
+ quality: 80
49
+ }
50
+ }
51
+ end
52
+
53
+ # Is it possible to create a ActiveStorage::Variant?
54
+ def variable?
55
+ blob&.variable?
56
+ end
57
+
58
+ def icon?
59
+ is_defined?(:icon)
60
+ end
61
+
62
+ # https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
63
+ def icon_name
64
+ return icon if icon
65
+ type_map = {
66
+ image: %w[image/bmp image/gif image/vnd.microsoft.icon image/jpeg image/png image/svg+xml image/tiff image/webp],
67
+ play: %w[video/mp4 video/mpeg video/ogg video/mp2t video/webm video/3gpp video/3gpp2],
68
+ music: %w[audio/aac audio/midi audio/x-midi audio/mpeg audio/ogg audio/opus audio/wav audio/webm audio/3gpp audio/3gpp2],
69
+ word: %w[application/msword application/vnd.openxmlformats-officedocument.wordprocessingml.document],
70
+ ppt: %w[application/vnd.ms-powerpoint application/vnd.openxmlformats-officedocument.presentationml.presentation],
71
+ excel: %w[application/vnd.ms-excel application/vnd.openxmlformats-officedocument.spreadsheetml.sheet],
72
+ slides: %w[application/vnd.oasis.opendocument.presentation],
73
+ spreadsheet: %w[application/vnd.oasis.opendocument.spreadsheet],
74
+ richtext: %w[application/vnd.oasis.opendocument.text],
75
+ zip: %w[application/zip application/x-7z-compressed application/x-bzip application/x-bzip2 application/gzip application/vnd.rar],
76
+ pdf: %w[application/pdf]
77
+ }
78
+
79
+ type = type_map.find { |key, mime_types| mime_types.include?(mime_type) }
80
+ if type
81
+ ["file", "earmark", type.first].compact.join("-")
82
+ else
83
+ "question"
84
+ end
85
+ end
86
+
87
+ def svg?
88
+ blob&.content_type == "image/svg+xml"
89
+ end
90
+
91
+ def inline_svg(options = {})
92
+ blob.open do |file|
93
+ content = file.read
94
+ doc = Nokogiri::HTML::DocumentFragment.parse content
95
+ svg = doc.at_css 'svg'
96
+
97
+ # for security
98
+ doc.search('script').each do |src|
99
+ src.remove
100
+ end
101
+
102
+ options.each { |attr, value| svg[attr.to_s] = value }
103
+
104
+ doc.to_html.html_safe
105
+ end
106
+ end
107
+ end
108
+ end
@@ -1,4 +1,4 @@
1
- <div class="h-autocomplete d-none" data-autocomplete-target="dropdown">
1
+ <div class="formstrap-autocomplete d-none" data-autocomplete-target="dropdown">
2
2
  <% if collection.any? %>
3
3
  <%= render "formstrap/autocomplete/list" do %>
4
4
  <% collection.each do |value, name| %>
@@ -36,7 +36,7 @@
36
36
  <%= render "formstrap/input_group", file.input_group_options do %>
37
37
 
38
38
  <% if file.preview %>
39
- <div class="h-form-file-thumbnails" data-file-preview-target="thumbnails">
39
+ <div class="formstrap-file-thumbnails" data-file-preview-target="thumbnails">
40
40
 
41
41
  <!-- Render previews for attachments -->
42
42
  <%= form.fields_for(file.nested_attribute) do |ff| %>
@@ -46,13 +46,13 @@
46
46
  filename = attachment.blob.filename.to_s
47
47
  size = number_to_human_size(attachment.blob.byte_size)
48
48
  %>
49
- <div class="h-form-file-thumbnail" title="<%= "#{filename} (#{size})" %>" data-file-preview-target="thumbnail">
49
+ <div class="formstrap-file-thumbnail" title="<%= "#{filename} (#{size})" %>" data-file-preview-target="thumbnail">
50
50
  <%= ff.hidden_field(:id, disabled: !file.destroy) %>
51
51
  <%= ff.hidden_field(:_destroy, data: {"file-preview-target": "thumbnailDestroy"}, disabled: !file.destroy) %>
52
52
  <%= render "formstrap/shared/thumbnail", file: attachment %>
53
53
 
54
54
  <% if file.destroy %>
55
- <div class="h-form-file-thumbnail-remove" data-action="click->file-preview#remove" data-file-preview-name-param="<%= filename %>">
55
+ <div class="formstrap-file-thumbnail-remove" data-action="click->file-preview#remove" data-file-preview-name-param="<%= filename %>">
56
56
  <%= bootstrap_icon("x") %>
57
57
  </div>
58
58
  <% end %>
@@ -61,11 +61,11 @@
61
61
 
62
62
  <!-- Placeholder -->
63
63
  <% if file.dropzone %>
64
- <div class="h-dropzone-placeholder <%= "d-none" if file.attachments.any? %>" data-file-preview-target="placeholder" style="height: 100px;">
65
- <%= t("headmin.forms.file.placeholder", count: file.number_of_files) %>
64
+ <div class="formstrap-dropzone-placeholder <%= "d-none" if file.attachments.any? %>" data-file-preview-target="placeholder" style="height: 100px;">
65
+ <%= t(".placeholder", count: file.number_of_files) %>
66
66
  </div>
67
67
  <% else %>
68
- <div class="h-form-file-thumbnail <%= "d-none" if file.attachments.any? %>" title="<%= t("headmin.forms.file.not_found") %>" data-file-preview-target="placeholder">
68
+ <div class="formstrap-file-thumbnail <%= "d-none" if file.attachments.any? %>" title="<%= t(".not_found") %>" data-file-preview-target="placeholder">
69
69
  <%= render "formstrap/shared/thumbnail", icon: "plus" %>
70
70
  </div>
71
71
  <% end %>
@@ -74,11 +74,11 @@
74
74
 
75
75
  <!-- Template -->
76
76
  <template data-file-preview-target="template">
77
- <div class="h-form-file-thumbnail" title="" data-file-preview-target="thumbnail">
77
+ <div class="formstrap-file-thumbnail" title="" data-file-preview-target="thumbnail">
78
78
  <%= render "formstrap/shared/thumbnail" %>
79
79
 
80
80
  <% if file.destroy %>
81
- <div class="h-form-file-thumbnail-remove" data-action="click->file-preview#remove">
81
+ <div class="formstrap-file-thumbnail-remove" data-action="click->file-preview#remove">
82
82
  <%= bootstrap_icon("x") %>
83
83
  </div>
84
84
  <% end %>
@@ -15,6 +15,8 @@
15
15
  # * +wrapper+ - Hash with all options for the surrounding html tag
16
16
  # * +width+ - Width of the thumbnail
17
17
  # * +height+ - Height of the thumbnail
18
+ # * +exclude_models+ - Array of model names to exclude from selection (e.g. ["User", "Company"])
19
+ # * +edit_url+ - Placeholder link for the edit page of the attachment. Needs to contain a ":id" part, e.g. "admin_media_edit_url(":id"). This url be opened in a modal
18
20
  #
19
21
  # ==== References
20
22
  # https://headmin.dev/docs/forms/media
@@ -30,17 +32,17 @@
30
32
 
31
33
  <%= render "formstrap/wrapper", media.wrapper_options do %>
32
34
  <%= render "formstrap/label", media.label_options if media.prepend_label? %>
33
- <div class="h-form-file-thumbnails" data-media-target="thumbnails">
35
+ <div class="formstrap-file-thumbnails" data-media-target="thumbnails">
34
36
  <%= render "formstrap/media/validation", media.custom_validation_options %>
35
37
 
36
38
  <!-- Render previews for attachments -->
37
39
  <%= form.fields_for(media.nested_attribute, media.association_object) do |ff| %>
38
- <%= render "formstrap/media/item", media.item_options.merge(form: ff, url: formstrap_media_path(name: media.name, ids: media.blob_ids, min: media.min, max: media.max, mimetype: media.accept)) %>
40
+ <%= render "formstrap/media/item", media.item_options.merge(form: ff, edit_url: media.edit_modal_url(ff.object)) %>
39
41
  <% end %>
40
42
 
41
43
  <!-- Placeholder -->
42
44
  <div class="<%= "d-none" if media.attachments.any? %>" data-media-target="placeholder">
43
- <a href="<%= formstrap_media_path(name: media.name, ids: media.blob_ids, min: media.min, max: media.max, mimetype: media.accept) %>" data-turbo-frame="remote_modal" data-media-target="modalButton">
45
+ <a href="<%= media.modal_url %>" data-turbo-frame="modal" data-media-target="modalButton">
44
46
  <%= render "formstrap/shared/thumbnail", media.thumbnail_options %>
45
47
  </a>
46
48
  </div>
@@ -50,7 +52,7 @@
50
52
  <% association_object = ActiveStorage::Attachment.new %>
51
53
  <template data-media-target="template" data-template-id-regex="<%= association_object.object_id %>">
52
54
  <%= form.fields_for(media.nested_attribute, ActiveStorage::Attachment.new, child_index: association_object.object_id) do |ff| %>
53
- <%= render "formstrap/media/item", media.item_options.merge(form: ff, url: formstrap_media_path(name: media.name, ids: media.blob_ids, min: media.min, max: media.max, mimetype: media.accept)) %>
55
+ <%= render "formstrap/media/item", media.item_options.merge(form: ff, edit_url: media.edit_modal_url(ff.object)) %>
54
56
  <% end %>
55
57
  </template>
56
58
 
@@ -59,7 +59,7 @@
59
59
  <%= render "formstrap/label", form: form, attribute: attribute, text: label, required: required %>
60
60
  <% end %>
61
61
 
62
- <ul class="repeater list-group <%= "list-group-flush" if flush %> mb-3" data-controller="repeater" data-repeater-target="list" data-repeater-id-value="<%= repeater_id %>">
62
+ <ul class="formstrap-repeater list-group <%= "list-group-flush" if flush %> mb-3" data-controller="repeater" data-repeater-target="list" data-repeater-id-value="<%= repeater_id %>">
63
63
 
64
64
  <!-- Header -->
65
65
  <% if header %>
@@ -85,7 +85,7 @@
85
85
 
86
86
  <!-- Button -->
87
87
  <div
88
- class="btn btn-sm h-btn-outline-light"
88
+ class="btn btn-sm btn-outline-secondary"
89
89
  data-repeater-target="addButton"
90
90
  data-popup-target="button"
91
91
  data-popup-id="<%= "repeater-buttons-#{repeater_id}" %>"
@@ -101,7 +101,7 @@
101
101
  <div class="d-grid gap-2">
102
102
  <% template_names.each do |name| %>
103
103
  <div
104
- class="btn btn-sm h-btn-outline-light"
104
+ class="btn btn-sm btn-outline-secondary"
105
105
  data-popup-target="button"
106
106
  data-popup-id="<%= "repeater-buttons-#{repeater_id}" %>"
107
107
  data-action="click->repeater#addRow click->popup#close"
@@ -9,29 +9,32 @@
9
9
  # * +sort+ - Allow sorting by dragging items. `active_storage_attachments` must have a position column.
10
10
  # * +width+ - Width of the thumbnail
11
11
  # * +height+ - Height of the thumbnail
12
+ # * +edit_url+ - URL for the edit modal
12
13
  #
13
14
 
14
15
  media_item = Formstrap::MediaItemView.new(local_assigns)
15
16
  %>
16
17
 
17
- <div class="h-form-file-thumbnail media-drag-sort-handle" title="<%= "#{media_item.filename} (#{media_item.size})" %>" data-media-target="item">
18
+ <div class="formstrap-file-thumbnail media-drag-sort-handle" title="<%= "#{media_item.filename} (#{media_item.size})" %>" data-media-target="item">
18
19
  <%= form.hidden_field(:id) %>
19
20
  <%= form.hidden_field(:blob_id) %>
20
21
  <%= form.hidden_field(:position, value: media_item.position_value) if media_item.sort %>
21
22
  <%= form.hidden_field(:_destroy) %>
22
23
 
23
- <a href="<%= media_item.url %>" data-turbo-frame="remote_modal" data-media-target="modalButton">
24
+ <a href="<%= media_item.url %>" data-turbo-frame="modal" data-media-target="modalButton">
24
25
  <%= render "formstrap/shared/thumbnail", media_item.thumbnail_options %>
25
26
  </a>
26
27
 
27
- <div class="h-form-file-thumbnail-actions">
28
+ <div class="formstrap-file-thumbnail-actions">
28
29
  <!-- Edit -->
29
- <a href="<%= formstrap_media_item_path(id: media_item.id) %>" class="h-form-file-thumbnail-edit" data-turbo-frame="remote_modal" data-media-target="editButton">
30
- <%= bootstrap_icon("pencil") %>
31
- </a>
30
+ <% if media_item.edit_url %>
31
+ <a href="<%= media_item.edit_url %>" class="formstrap-file-thumbnail-edit" data-turbo-frame="modal" data-media-target="editButton">
32
+ <%= bootstrap_icon("pencil") %>
33
+ </a>
34
+ <% end %>
32
35
 
33
36
  <!-- Remove -->
34
- <div class="h-form-file-thumbnail-remove" data-action="click->media#destroy">
37
+ <div class="formstrap-file-thumbnail-remove" data-action="click->media#destroy">
35
38
  <%= bootstrap_icon("x") %>
36
39
  </div>
37
40
  </div>
@@ -1,11 +1,30 @@
1
- <div class="media-modal modal fade" tabindex="-1" data-controller="remote-modal media-modal" data-media-modal-ids-value="<%= params[:ids] ? params[:ids] : [] %>" data-name="<%= name %>" data-min="<%= min %>" data-max="<%= max %>">
1
+ <div class="formstrap-media-modal modal fade" tabindex="-1" data-controller="modal media-modal" data-media-modal-ids-value="<%= params[:ids] ? params[:ids] : [] %>" data-name="<%= name %>" data-min="<%= min %>" data-max="<%= max %>">
2
2
  <div class="modal-dialog modal-lg modal-dialog-scrollable">
3
3
  <div class="modal-content">
4
4
  <div class="modal-header">
5
5
  <h5 class="modal-title">
6
6
  <%= t(".title", count: min.to_i < 1 ? 1 : min.to_i) %>
7
7
  </h5>
8
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="<%= t(".close") %>"></button>
8
+
9
+ <div class="modal-header-actions d-flex align-items-center gap-3">
10
+ <%= formstrap_form_with url: formstrap_media_path, method: :get, data: {turbo_frame: "modal", media_modal_target: "search"}, class: "modal-search d-flex gap-1" do |form| %>
11
+ <%= form.hidden_field :name, value: request.query_parameters["name"] %>
12
+ <%= form.hidden_field :min, value: request.query_parameters["min"] %>
13
+ <%= form.hidden_field :max, value: request.query_parameters["max"] %>
14
+ <%= form.hidden_field :mimetype, value: request.query_parameters["mimetype"] %>
15
+ <%= form.hidden_field "exclude_models[]", value: request.query_parameters["exclude_models"] %>
16
+
17
+ <% if request.query_parameters["ids"] %>
18
+ <% request.query_parameters["ids"].each do |id| %>
19
+ <%= form.hidden_field "ids[]", value: id, data: {media_modal_target: "searchId"} %>
20
+ <% end %>
21
+ <% end %>
22
+
23
+ <%= form.search :search, value: request.query_parameters["search"], class: "form-control", label: false, wrapper: {class: ""}, placeholder: t(".search") %>
24
+ <% end %>
25
+
26
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="<%= t(".close") %>"></button>
27
+ </div>
9
28
  </div>
10
29
  <div class="modal-body">
11
30
  <%= turbo_frame_tag "thumbnails", class: "d-flex flex-wrap gap-2" do %>
@@ -14,17 +33,25 @@
14
33
  <%= render "thumbnail" %>
15
34
  <% end %>
16
35
  <% end %>
17
- <div data-media-modal-target="placeholder" class="<%= "d-none" if !@blobs.empty? %>">
36
+
37
+ <div data-media-modal-target="placeholder" class="<%= "d-none" if !@blobs.empty? && request.request_parameters["search"].nil? %>">
18
38
  <p><%= t(".placeholder") %></p>
19
39
  </div>
40
+
41
+ <div data-media-modal-target="search_no_results" class="<%= "d-none" if !@blobs.empty? || request.request_parameters["search"].present? %>">
42
+ <p><%= t(".search_no_results") %></p>
43
+ </div>
44
+ <% end %>
45
+
46
+ <% unless @blobs.empty? %>
47
+ <div class="mt-3">
48
+ <%= render "formstrap/pagination/infinite", items: @blobs %>
49
+ </div>
20
50
  <% end %>
21
- <div class="mt-3">
22
- <%= render "formstrap/pagination/infinite", items: @blobs %>
23
- </div>
24
51
  </div>
25
52
  <div class="modal-footer">
26
- <%= form_with url: formstrap_new_media_path, multipart: true, data: {"media-modal-target": "form"}, class: "me-auto" do |form| %>
27
- <%= form.label :files, class: "btn h-btn-outline-light" do %>
53
+ <%= formstrap_form_with url: formstrap_new_media_path, multipart: true, data: {"media-modal-target": "form", turbo_frame: "modal"}, class: "me-auto" do |form| %>
54
+ <%= form.label :files, class: "btn btn-outline-secondary" do %>
28
55
  <%= bootstrap_icon("upload") %>
29
56
  <%= t(".upload") %>
30
57
  <%= form.file_field :files, accept: mimetypes, class: "d-none", multiple: true, data: {action: "change->media-modal#submitForm"} %>
@@ -3,18 +3,22 @@
3
3
  <div data-media-modal-target="item" title="<%= "#{blob.filename} (#{l(blob.created_at, format: :long)})" %>">
4
4
  <!-- Input -->
5
5
  <input
6
- id="media-item-<%= blob.id %>"
7
- type="checkbox"
8
- value="<%= blob.id %>"
9
- data-action="change->media-modal#inputChange"
10
- data-media-modal-target="idCheckbox"
11
- hidden>
6
+ id="media-item-<%= blob.id %>"
7
+ type="checkbox"
8
+ value="<%= blob.id %>"
9
+ data-action="change->media-modal#inputChange"
10
+ data-media-modal-target="idCheckbox"
11
+ hidden>
12
12
 
13
13
  <!-- Label -->
14
- <label for="media-item-<%= blob.id %>">
14
+ <label for="media-item-<%= blob.id %>" class="d-flex">
15
15
  <%= render "formstrap/shared/thumbnail", file: blob %>
16
16
  </label>
17
17
  </div>
18
18
  <% else %>
19
- <%= render "formstrap/shared/thumbnail", file: nil %>
19
+ <div>
20
+ <label class="d-flex">
21
+ <%= render "formstrap/shared/thumbnail", file: nil %>
22
+ </label>
23
+ </div>
20
24
  <% end %>
@@ -2,7 +2,7 @@
2
2
  <%= form.text_field :"validation_#{attribute}",
3
3
  name: nil,
4
4
  value: nil,
5
- class: "h-form-media-validation",
5
+ class: "formstrap-media-validation",
6
6
  data: {
7
7
  "media-target": "validationInput",
8
8
  "min-message": t(".min", count: min),
@@ -1,5 +1,7 @@
1
1
  <%= turbo_stream.prepend "thumbnails" do %>
2
2
  <% @blobs.each do |blob| %>
3
- <%= render "formstrap/media/thumbnail", blob: blob %>
3
+ <turbo-frame id="blob_<%= blob.id %>">
4
+ <%= render "formstrap/media/thumbnail", blob: blob %>
5
+ </turbo-frame>
4
6
  <% end %>
5
7
  <% end %>
@@ -1,3 +1,3 @@
1
- <%= turbo_frame_tag "remote_modal" do %>
1
+ <%= turbo_frame_tag "modal" do %>
2
2
  <%= render "formstrap/media/modal", blobs: @blobs, mimetypes: @mimetypes, name: params[:name], min: params[:min], max: params[:max] %>
3
3
  <% end %>