headmin 0.3.4 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +27 -0
  3. data/.gitignore +14 -0
  4. data/.lock-487e157d270f3062a98b7b2a012753708-1272821827 +0 -0
  5. data/.nvmrc +1 -0
  6. data/CHANGELOG.md +31 -0
  7. data/Gemfile +7 -4
  8. data/Gemfile.lock +191 -25
  9. data/README.md +7 -0
  10. data/Rakefile +1 -7
  11. data/app/assets/javascripts/headmin/config/i18n.js +9 -9
  12. data/app/assets/javascripts/headmin/controllers/autocomplete_controller.js +318 -0
  13. data/app/assets/javascripts/headmin/controllers/blocks_controller.js +74 -79
  14. data/app/assets/javascripts/headmin/controllers/date_range_controller.js +24 -24
  15. data/app/assets/javascripts/headmin/controllers/dropzone_controller.js +23 -25
  16. data/app/assets/javascripts/headmin/controllers/file_preview_controller.js +237 -237
  17. data/app/assets/javascripts/headmin/controllers/filter_controller.js +44 -44
  18. data/app/assets/javascripts/headmin/controllers/filters_controller.js +57 -61
  19. data/app/assets/javascripts/headmin/controllers/flatpickr_controller.js +29 -29
  20. data/app/assets/javascripts/headmin/controllers/hello_controller.js +3 -3
  21. data/app/assets/javascripts/headmin/controllers/notification_controller.js +7 -6
  22. data/app/assets/javascripts/headmin/controllers/popup_controller.js +57 -51
  23. data/app/assets/javascripts/headmin/controllers/redactorx_controller.js +36 -9
  24. data/app/assets/javascripts/headmin/controllers/repeater_controller.js +122 -125
  25. data/app/assets/javascripts/headmin/controllers/select_controller.js +40 -39
  26. data/app/assets/javascripts/headmin/controllers/table_actions_controller.js +100 -101
  27. data/app/assets/javascripts/headmin/controllers/table_controller.js +115 -115
  28. data/app/assets/javascripts/headmin/index.js +38 -35
  29. data/app/assets/javascripts/headmin.js +295 -37
  30. data/app/assets/stylesheets/headmin/forms/autocomplete.scss +21 -0
  31. data/app/assets/stylesheets/headmin/forms/file.scss +46 -0
  32. data/app/assets/stylesheets/headmin/forms/repeater.scss +62 -0
  33. data/app/assets/stylesheets/headmin/forms/search.scss +12 -0
  34. data/app/assets/stylesheets/headmin/forms.scss +11 -0
  35. data/app/assets/stylesheets/headmin/general.scss +14 -0
  36. data/app/assets/stylesheets/headmin/overrides/bootstrap.scss +5 -3
  37. data/app/assets/stylesheets/headmin/overrides/redactorx.scss +74 -0
  38. data/app/assets/stylesheets/headmin/popup.scss +1 -0
  39. data/app/assets/stylesheets/headmin/syntax.scss +36 -349
  40. data/app/assets/stylesheets/headmin/table.scss +1 -1
  41. data/app/assets/stylesheets/headmin/utilities/buttons.scss +19 -0
  42. data/app/assets/stylesheets/headmin/utilities/dropzone.scss +72 -0
  43. data/app/assets/stylesheets/headmin/utilities.scss +2 -68
  44. data/app/assets/stylesheets/headmin.css +209 -205
  45. data/app/assets/stylesheets/headmin.scss +1 -1
  46. data/app/helpers/headmin/admin_helper.rb +0 -1
  47. data/app/helpers/headmin/form_helper.rb +2 -8
  48. data/app/models/concerns/headmin/blockable.rb +1 -1
  49. data/app/models/concerns/headmin/field.rb +4 -1
  50. data/app/models/concerns/headmin/fieldable.rb +138 -44
  51. data/app/models/concerns/headmin/form/autocompletable.rb +38 -0
  52. data/app/models/concerns/headmin/form/hintable.rb +19 -0
  53. data/app/models/concerns/headmin/form/input_groupable.rb +23 -0
  54. data/app/models/concerns/headmin/form/labelable.rb +33 -0
  55. data/app/models/concerns/headmin/form/listable.rb +28 -0
  56. data/app/models/concerns/headmin/form/placeholderable.rb +13 -0
  57. data/app/models/concerns/headmin/form/validatable.rb +40 -0
  58. data/app/models/concerns/headmin/form/wrappable.rb +21 -0
  59. data/app/models/headmin/.DS_Store +0 -0
  60. data/app/models/headmin/blocks_view.rb +15 -0
  61. data/app/models/headmin/form/blocks_view.rb +29 -0
  62. data/app/models/headmin/form/checkbox_view.rb +52 -0
  63. data/app/models/headmin/form/date_range_view.rb +25 -0
  64. data/app/models/headmin/form/date_view.rb +45 -0
  65. data/app/models/headmin/form/email_view.rb +48 -0
  66. data/app/models/headmin/form/file_view.rb +116 -0
  67. data/app/models/headmin/form/flatpickr_range_view.rb +102 -0
  68. data/app/models/headmin/form/flatpickr_view.rb +37 -0
  69. data/app/models/headmin/form/hidden_view.rb +10 -0
  70. data/app/models/headmin/form/hint_view.rb +6 -0
  71. data/app/models/headmin/form/input_group_view.rb +19 -0
  72. data/app/models/headmin/form/label_view.rb +24 -0
  73. data/app/models/headmin/form/number_view.rb +49 -0
  74. data/app/models/headmin/form/password_view.rb +44 -0
  75. data/app/models/headmin/form/redactorx_view.rb +59 -0
  76. data/app/models/headmin/form/search_view.rb +48 -0
  77. data/app/models/headmin/form/select_view.rb +62 -0
  78. data/app/models/headmin/form/switch_view.rb +23 -0
  79. data/app/models/headmin/form/text_view.rb +48 -0
  80. data/app/models/headmin/form/textarea_view.rb +44 -0
  81. data/app/models/headmin/form/url_view.rb +48 -0
  82. data/app/models/headmin/form/wrapper_view.rb +19 -0
  83. data/app/models/headmin/form/wysiwyg_view.rb +17 -0
  84. data/app/models/headmin/{thumbnail.rb → thumbnail_view.rb} +6 -1
  85. data/app/models/view_model.rb +58 -0
  86. data/app/views/headmin/_blocks.html.erb +13 -9
  87. data/app/views/headmin/_heading.html.erb +7 -1
  88. data/app/views/headmin/_thumbnail.html.erb +1 -37
  89. data/app/views/headmin/forms/_autocomplete.html.erb +11 -0
  90. data/app/views/headmin/forms/_blocks.html.erb +16 -17
  91. data/app/views/headmin/forms/_checkbox.html.erb +24 -29
  92. data/app/views/headmin/forms/_datalist.html.erb +3 -0
  93. data/app/views/headmin/forms/_date.html.erb +24 -24
  94. data/app/views/headmin/forms/_date_range.html.erb +19 -21
  95. data/app/views/headmin/forms/_email.html.erb +27 -32
  96. data/app/views/headmin/forms/_errors.html.erb +2 -3
  97. data/app/views/headmin/forms/_file.html.erb +84 -181
  98. data/app/views/headmin/forms/_flatpickr.html.erb +19 -20
  99. data/app/views/headmin/forms/_flatpickr_range.html.erb +28 -37
  100. data/app/views/headmin/forms/_hidden.html.erb +9 -10
  101. data/app/views/headmin/forms/_hint.html.erb +16 -0
  102. data/app/views/headmin/forms/_input_group.html.erb +21 -0
  103. data/app/views/headmin/forms/_label.html.erb +5 -13
  104. data/app/views/headmin/forms/_number.html.erb +23 -35
  105. data/app/views/headmin/forms/_password.html.erb +21 -30
  106. data/app/views/headmin/forms/_redactorx.html.erb +21 -40
  107. data/app/views/headmin/forms/_repeater.html.erb +55 -60
  108. data/app/views/headmin/forms/_search.html.erb +43 -0
  109. data/app/views/headmin/forms/_select.html.erb +24 -49
  110. data/app/views/headmin/forms/_switch.html.erb +29 -0
  111. data/app/views/headmin/forms/_text.html.erb +42 -96
  112. data/app/views/headmin/forms/_textarea.html.erb +21 -32
  113. data/app/views/headmin/forms/_url.html.erb +26 -31
  114. data/app/views/headmin/forms/_validation.html.erb +10 -13
  115. data/app/views/headmin/forms/_wrapper.html.erb +9 -0
  116. data/app/views/headmin/forms/_wysiwyg.html.erb +28 -0
  117. data/app/views/headmin/forms/autocomplete/_item.html.erb +3 -0
  118. data/app/views/headmin/forms/autocomplete/_list.html.erb +3 -0
  119. data/app/views/headmin/forms/fields/_file.html.erb +1 -1
  120. data/app/views/headmin/forms/fields/_files.html.erb +17 -0
  121. data/app/views/headmin/forms/fields/_group.html.erb +9 -2
  122. data/app/views/headmin/forms/repeater/_row.html.erb +4 -4
  123. data/bin/console +0 -1
  124. data/config/locales/headmin/forms/en.yml +1 -12
  125. data/config/locales/headmin/forms/nl.yml +1 -12
  126. data/esbuild-css.js +18 -18
  127. data/esbuild-js.js +8 -8
  128. data/headmin.gemspec +1 -3
  129. data/lib/generators/templates/controllers/auth/confirmations_controller.rb +0 -2
  130. data/lib/generators/templates/controllers/auth/omniauth_callbacks_controller.rb +0 -2
  131. data/lib/generators/templates/controllers/auth/passwords_controller.rb +0 -2
  132. data/lib/generators/templates/controllers/auth/registrations_controller.rb +0 -2
  133. data/lib/generators/templates/controllers/auth/sessions_controller.rb +0 -2
  134. data/lib/generators/templates/controllers/auth/unlocks_controller.rb +0 -2
  135. data/lib/headmin/version.rb +1 -3
  136. data/lib/headmin.rb +0 -2
  137. data/package-lock.json +5359 -0
  138. data/package.json +12 -4
  139. data/view_model_benchmark.rb +74 -0
  140. data/yarn-error.log +17 -12
  141. data/yarn.lock +1575 -31
  142. metadata +64 -25
  143. data/app/assets/stylesheets/headmin/form.scss +0 -132
  144. data/app/assets/stylesheets/headmin/overrides/redactorx.css +0 -3
  145. data/app/helpers/headmin/documentation_helper.rb +0 -35
  146. data/app/models/headmin/documentation_renderer.rb +0 -32
  147. data/app/models/headmin/form/base.rb +0 -78
  148. data/app/models/headmin/form/text.rb +0 -51
  149. data/app/services/block_service.rb +0 -72
  150. data/app/views/headmin/_card.html.erb +0 -52
  151. data/app/views/headmin/forms/_actions.html.erb +0 -28
  152. data/app/views/headmin/forms/_base.html.erb +0 -114
  153. data/app/views/headmin/forms/_image.html.erb +0 -21
  154. data/app/views/headmin/forms/_video.html.erb +0 -21
  155. data/app/views/headmin/forms/actions/_destroy.html.erb +0 -13
  156. data/app/views/headmin/forms/actions/_save.html.erb +0 -12
  157. data/app/views/headmin/forms/actions/_view.html.erb +0 -15
  158. data/app/views/headmin/forms/fields/_image.html.erb +0 -17
  159. data/docs/blocks-and-fields.md +0 -54
  160. data/docs/blocks.md +0 -48
  161. data/docs/devise.md +0 -41
  162. data/docs/fields.md +0 -79
@@ -1,244 +1,244 @@
1
- import {Controller} from "@hotwired/stimulus"
1
+ /* global FileReader, DataTransfer */
2
+ import { Controller } from '@hotwired/stimulus'
2
3
 
3
4
  // References:
4
5
  // https://developer.mozilla.org/en-US/docs/Web/API/FileReader
5
6
  // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file
6
7
 
7
8
  export default class extends Controller {
8
- static get targets() {
9
- return ["thumbnails", "template", "input", "placeholder", "thumbnail", "thumbnailDestroy"]
10
- }
11
-
12
- connect() {
13
- this.thumbnailWidth = this.firstThumbnailWidth()
14
- this.thumbnailHeight = this.firstThumbnailHeight()
15
- }
16
-
17
- preview() {
18
- this.removeThumbnails()
19
- this.addThumbnails()
20
- this.togglePlaceholder()
21
- }
22
-
23
- togglePlaceholder() {
24
- if (this.hasFilesSelected()) {
25
- this.hidePlaceholder()
26
- } else {
27
- this.showPlaceholder()
28
- }
29
- }
30
-
31
- hasFilesSelected() {
32
- return this.hasInputFiles() || this.hasVisibleAttachments()
33
- }
34
-
35
- hasVisibleAttachments() {
36
- return this.visibleAttachments().length > 0
37
- }
38
-
39
- hasAttachments() {
40
- return this.attachments().length > 0
41
- }
42
-
43
- hasInputFiles() {
44
- return this.inputFiles().length > 0
45
- }
46
-
47
- attachments() {
48
- return this.thumbnailTargets.filter((thumbnail) => {
49
- return this.isAttachment(thumbnail)
50
- })
51
- }
52
-
53
- visibleAttachments() {
54
- return this.attachments().filter((thumbnail) => {
55
- return !thumbnail.classList.contains('d-none')
56
- })
57
- }
58
-
59
- inputFiles() {
60
- return Array.from(this.inputTarget.files)
61
- }
62
-
63
- writeInputFiles(files) {
64
- const dataTransfer = new DataTransfer();
65
- files.forEach((file) => {
66
- dataTransfer.items.add(file);
67
- })
68
- this.inputTarget.files = dataTransfer.files;
69
- }
70
-
71
- remove(event) {
72
- const thumbnail = event.target.closest('[data-file-preview-target="thumbnail"]')
73
- this.removeThumbnail(thumbnail, event.params.name)
74
- this.togglePlaceholder()
75
- }
76
-
77
- isAttachment(thumbnail) {
78
- return thumbnail.querySelector('[data-file-preview-target="thumbnailDestroy"]')
79
- }
80
-
81
- removeInputFile(thumbnail, fileName) {
82
- let files = this.inputFiles()
83
- files = files.filter((file) => {
84
- return file.name !== fileName
85
- })
86
- thumbnail.remove()
87
- this.writeInputFiles(files)
88
- }
89
-
90
- removeAttachment(thumbnail) {
91
- const destroyInput = thumbnail.querySelector('[data-file-preview-target="thumbnailDestroy"]')
92
- destroyInput.value = '1';
93
- thumbnail.classList.add('d-none')
94
- }
95
-
96
- showPlaceholder() {
97
- this.placeholderTarget.classList.remove('d-none')
98
- }
99
-
100
- hidePlaceholder() {
101
- this.placeholderTarget.classList.add('d-none')
102
- }
103
-
104
- removeThumbnails() {
105
- this.thumbnailTargets.forEach((thumbnail) => {
106
- this.removeThumbnail(thumbnail)
107
- })
108
- }
109
-
110
- removeThumbnail(thumbnail, filename) {
111
- if (this.isAttachment(thumbnail)) {
112
- this.removeAttachment(thumbnail)
113
- } else {
114
- this.removeInputFile(thumbnail, filename)
115
- }
116
- }
117
-
118
- addThumbnails() {
119
- const files = this.inputFiles()
120
- files.forEach((file) => {
121
- let thumbnail = this.generateDummyThumbnail()
122
- this.appendThumbnail(thumbnail)
123
- this.updateThumbnail(this.lastThumbnail(), file)
124
- })
125
- }
126
-
127
- generateDummyThumbnail() {
128
- return this.templateTarget.content.cloneNode(true)
129
- }
130
-
131
- appendThumbnail(thumbnail) {
132
- return this.thumbnailsTarget.appendChild(thumbnail)
133
- }
134
-
135
- updateThumbnail(thumbnail, file) {
136
- this.updateThumbnailRemoveButton(thumbnail, file.name)
137
-
138
- const title = this.titleForFile(file)
139
- this.updateThumbnailTitle(thumbnail, title)
140
-
141
- const icon = this.iconForMimeType(file.type)
142
- this.updateThumbnailIcon(thumbnail, icon)
143
-
144
- if (this.isImage(file)) {
145
- this.updateThumbnailImage(thumbnail, file)
146
- }
147
-
148
- }
149
-
150
- updateThumbnailImage(thumbnail, file) {
151
- this.fileToBase64(file).then(base64 => {
152
- this.updateThumbnailBackground(thumbnail, base64)
153
- this.removeThumbnailIcon(thumbnail)
154
- })
155
- }
156
-
157
- titleForFile(file) {
158
- const byteSizeString = this.bytesToString(file.size)
159
- return `${file.name} (${byteSizeString})`
160
- }
161
-
162
- bytesToString(bytes) {
163
- const i = Math.floor(Math.log(bytes) / Math.log(1024));
164
- return (bytes / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i];
165
- }
166
-
167
- updateThumbnailRemoveButton(thumbnail, fileName) {
168
- const removeButton = thumbnail.querySelector('.h-form-file-thumbnail-remove')
169
- if(removeButton) {
170
- removeButton.dataset.filePreviewNameParam = fileName
171
- }
172
- }
173
-
174
- updateThumbnailTitle(thumbnail, title) {
175
- thumbnail.title = title
176
- }
177
-
178
- updateThumbnailBackground(thumbnail, url) {
179
- let thumbnailBackground = thumbnail.querySelector('.h-thumbnail-bg')
180
- thumbnailBackground.style.backgroundImage = `url('${url}')`
181
- }
182
-
183
- removeThumbnailIcon(thumbnail) {
184
- thumbnail.querySelector('.h-thumbnail-bg').innerHTML = ''
185
- }
186
-
187
- updateThumbnailIcon(thumbnail, icon) {
188
- thumbnail.querySelector('.h-thumbnail-bg').innerHTML = icon
189
- }
190
-
191
- iconForMimeType(mimeType) {
192
- const typeMap = {
193
- image: ['image/bmp', 'image/gif', 'image/vnd.microsoft.icon', 'image/jpeg', 'image/png', 'image/svg+xml', 'image/tiff', 'image/webp'],
194
- play: ['video/mp4', 'video/mpeg', 'video/ogg', 'video/mp2t', 'video/webm', 'video/3gpp', 'video/3gpp2'],
195
- music: ['audio/aac', 'audio/midi', 'audio/x-midi', 'audio/mpeg', 'audio/ogg', 'audio/opus', 'audio/wav', 'audio/webm', 'audio/3gpp', 'audio/3gpp2'],
196
- word: ['application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
197
- ppt: ['application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation'],
198
- excel: ['application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
199
- slides: ['application/vnd.oasis.opendocument.presentation'],
200
- spreadsheet: ['application/vnd.oasis.opendocument.spreadsheet'],
201
- richtext: ['application/vnd.oasis.opendocument.text'],
202
- zip: ['application/zip application/x-7z-compressed', 'application/x-bzip application/x-bzip2 application/gzip application/vnd.rar'],
203
- pdf: ['application/pdf']
204
- }
205
-
206
- const icon_name = Object.keys(typeMap).find(key => typeMap[key].includes(mimeType));
207
- const full_icon_name = ['bi', 'file', 'earmark', icon_name].filter(e => typeof e === 'string' && e !== '').join('-')
208
-
209
- return `<i class="bi ${full_icon_name} h-thumbnail-icon"></i>`
210
- }
211
-
212
- isImage(file) {
213
- return file.type.match(/^image/) !== null;
214
- }
215
-
216
- fileToBase64(file) {
217
- return new Promise((resolve, reject) => {
218
- const reader = new FileReader();
219
- reader.readAsDataURL(file);
220
- reader.onload = () => resolve(reader.result);
221
- reader.onerror = error => reject(error);
222
- });
223
- }
224
-
225
- thumbnails() {
226
- return this.thumbnailsTarget.querySelectorAll('.img-thumbnail')
227
- }
228
-
229
- firstThumbnail() {
230
- return this.thumbnailsTarget.firstElementChild
231
- }
232
-
233
- lastThumbnail() {
234
- return this.thumbnailsTarget.lastElementChild
235
- }
236
-
237
- firstThumbnailWidth() {
238
- return this.firstThumbnail().style.width;
239
- }
240
-
241
- firstThumbnailHeight() {
242
- return this.firstThumbnail().style.height;
243
- }
9
+ static get targets () {
10
+ return ['thumbnails', 'template', 'input', 'placeholder', 'thumbnail', 'thumbnailDestroy']
11
+ }
12
+
13
+ connect () {
14
+ this.thumbnailWidth = this.firstThumbnailWidth()
15
+ this.thumbnailHeight = this.firstThumbnailHeight()
16
+ }
17
+
18
+ preview () {
19
+ this.removeThumbnails()
20
+ this.addThumbnails()
21
+ this.togglePlaceholder()
22
+ }
23
+
24
+ togglePlaceholder () {
25
+ if (this.hasFilesSelected()) {
26
+ this.hidePlaceholder()
27
+ } else {
28
+ this.showPlaceholder()
29
+ }
30
+ }
31
+
32
+ hasFilesSelected () {
33
+ return this.hasInputFiles() || this.hasVisibleAttachments()
34
+ }
35
+
36
+ hasVisibleAttachments () {
37
+ return this.visibleAttachments().length > 0
38
+ }
39
+
40
+ hasAttachments () {
41
+ return this.attachments().length > 0
42
+ }
43
+
44
+ hasInputFiles () {
45
+ return this.inputFiles().length > 0
46
+ }
47
+
48
+ attachments () {
49
+ return this.thumbnailTargets.filter((thumbnail) => {
50
+ return this.isAttachment(thumbnail)
51
+ })
52
+ }
53
+
54
+ visibleAttachments () {
55
+ return this.attachments().filter((thumbnail) => {
56
+ return !thumbnail.classList.contains('d-none')
57
+ })
58
+ }
59
+
60
+ inputFiles () {
61
+ return Array.from(this.inputTarget.files)
62
+ }
63
+
64
+ writeInputFiles (files) {
65
+ const dataTransfer = new DataTransfer()
66
+ files.forEach((file) => {
67
+ dataTransfer.items.add(file)
68
+ })
69
+ this.inputTarget.files = dataTransfer.files
70
+ }
71
+
72
+ remove (event) {
73
+ const thumbnail = event.target.closest('[data-file-preview-target="thumbnail"]')
74
+ this.removeThumbnail(thumbnail, event.params.name)
75
+ this.togglePlaceholder()
76
+ }
77
+
78
+ isAttachment (thumbnail) {
79
+ return thumbnail.querySelector('[data-file-preview-target="thumbnailDestroy"]')
80
+ }
81
+
82
+ removeInputFile (thumbnail, fileName) {
83
+ let files = this.inputFiles()
84
+ files = files.filter((file) => {
85
+ return file.name !== fileName
86
+ })
87
+ thumbnail.remove()
88
+ this.writeInputFiles(files)
89
+ }
90
+
91
+ removeAttachment (thumbnail) {
92
+ const destroyInput = thumbnail.querySelector('[data-file-preview-target="thumbnailDestroy"]')
93
+ destroyInput.value = '1'
94
+ thumbnail.classList.add('d-none')
95
+ }
96
+
97
+ showPlaceholder () {
98
+ this.placeholderTarget.classList.remove('d-none')
99
+ }
100
+
101
+ hidePlaceholder () {
102
+ this.placeholderTarget.classList.add('d-none')
103
+ }
104
+
105
+ removeThumbnails () {
106
+ this.thumbnailTargets.forEach((thumbnail) => {
107
+ this.removeThumbnail(thumbnail)
108
+ })
109
+ }
110
+
111
+ removeThumbnail (thumbnail, filename) {
112
+ if (this.isAttachment(thumbnail)) {
113
+ this.removeAttachment(thumbnail)
114
+ } else {
115
+ this.removeInputFile(thumbnail, filename)
116
+ }
117
+ }
118
+
119
+ addThumbnails () {
120
+ const files = this.inputFiles()
121
+ files.forEach((file) => {
122
+ const thumbnail = this.generateDummyThumbnail()
123
+ this.appendThumbnail(thumbnail)
124
+ this.updateThumbnail(this.lastThumbnail(), file)
125
+ })
126
+ }
127
+
128
+ generateDummyThumbnail () {
129
+ return this.templateTarget.content.cloneNode(true)
130
+ }
131
+
132
+ appendThumbnail (thumbnail) {
133
+ return this.thumbnailsTarget.appendChild(thumbnail)
134
+ }
135
+
136
+ updateThumbnail (thumbnail, file) {
137
+ this.updateThumbnailRemoveButton(thumbnail, file.name)
138
+
139
+ const title = this.titleForFile(file)
140
+ this.updateThumbnailTitle(thumbnail, title)
141
+
142
+ const icon = this.iconForMimeType(file.type)
143
+ this.updateThumbnailIcon(thumbnail, icon)
144
+
145
+ if (this.isImage(file)) {
146
+ this.updateThumbnailImage(thumbnail, file)
147
+ }
148
+ }
149
+
150
+ updateThumbnailImage (thumbnail, file) {
151
+ this.fileToBase64(file).then(base64 => {
152
+ this.updateThumbnailBackground(thumbnail, base64)
153
+ this.removeThumbnailIcon(thumbnail)
154
+ })
155
+ }
156
+
157
+ titleForFile (file) {
158
+ const byteSizeString = this.bytesToString(file.size)
159
+ return `${file.name} (${byteSizeString})`
160
+ }
161
+
162
+ bytesToString (bytes) {
163
+ const i = Math.floor(Math.log(bytes) / Math.log(1024))
164
+ return (bytes / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]
165
+ }
166
+
167
+ updateThumbnailRemoveButton (thumbnail, fileName) {
168
+ const removeButton = thumbnail.querySelector('.h-form-file-thumbnail-remove')
169
+ if (removeButton) {
170
+ removeButton.dataset.filePreviewNameParam = fileName
171
+ }
172
+ }
173
+
174
+ updateThumbnailTitle (thumbnail, title) {
175
+ thumbnail.title = title
176
+ }
177
+
178
+ updateThumbnailBackground (thumbnail, url) {
179
+ const thumbnailBackground = thumbnail.querySelector('.h-thumbnail-bg')
180
+ thumbnailBackground.style.backgroundImage = `url('${url}')`
181
+ }
182
+
183
+ removeThumbnailIcon (thumbnail) {
184
+ thumbnail.querySelector('.h-thumbnail-bg').innerHTML = ''
185
+ }
186
+
187
+ updateThumbnailIcon (thumbnail, icon) {
188
+ thumbnail.querySelector('.h-thumbnail-bg').innerHTML = icon
189
+ }
190
+
191
+ iconForMimeType (mimeType) {
192
+ const typeMap = {
193
+ image: ['image/bmp', 'image/gif', 'image/vnd.microsoft.icon', 'image/jpeg', 'image/png', 'image/svg+xml', 'image/tiff', 'image/webp'],
194
+ play: ['video/mp4', 'video/mpeg', 'video/ogg', 'video/mp2t', 'video/webm', 'video/3gpp', 'video/3gpp2'],
195
+ music: ['audio/aac', 'audio/midi', 'audio/x-midi', 'audio/mpeg', 'audio/ogg', 'audio/opus', 'audio/wav', 'audio/webm', 'audio/3gpp', 'audio/3gpp2'],
196
+ word: ['application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
197
+ ppt: ['application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation'],
198
+ excel: ['application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
199
+ slides: ['application/vnd.oasis.opendocument.presentation'],
200
+ spreadsheet: ['application/vnd.oasis.opendocument.spreadsheet'],
201
+ richtext: ['application/vnd.oasis.opendocument.text'],
202
+ zip: ['application/zip application/x-7z-compressed', 'application/x-bzip application/x-bzip2 application/gzip application/vnd.rar'],
203
+ pdf: ['application/pdf']
204
+ }
205
+
206
+ const iconName = Object.keys(typeMap).find(key => typeMap[key].includes(mimeType))
207
+ const fullIconName = ['bi', 'file', 'earmark', iconName].filter(e => typeof e === 'string' && e !== '').join('-')
208
+
209
+ return `<i class="bi ${fullIconName} h-thumbnail-icon"></i>`
210
+ }
211
+
212
+ isImage (file) {
213
+ return file.type.match(/^image/) !== null
214
+ }
215
+
216
+ fileToBase64 (file) {
217
+ return new Promise((resolve, reject) => {
218
+ const reader = new FileReader()
219
+ reader.readAsDataURL(file)
220
+ reader.onload = () => resolve(reader.result)
221
+ reader.onerror = error => reject(error)
222
+ })
223
+ }
224
+
225
+ thumbnails () {
226
+ return this.thumbnailsTarget.querySelectorAll('.img-thumbnail')
227
+ }
228
+
229
+ firstThumbnail () {
230
+ return this.thumbnailsTarget.firstElementChild
231
+ }
232
+
233
+ lastThumbnail () {
234
+ return this.thumbnailsTarget.lastElementChild
235
+ }
236
+
237
+ firstThumbnailWidth () {
238
+ return this.firstThumbnail().style.width
239
+ }
240
+
241
+ firstThumbnailHeight () {
242
+ return this.firstThumbnail().style.height
243
+ }
244
244
  }
@@ -1,53 +1,53 @@
1
- import {Controller} from "@hotwired/stimulus"
1
+ import { Controller } from '@hotwired/stimulus'
2
2
 
3
3
  export default class extends Controller {
4
- static get targets() {
5
- return ["button", "popup"]
4
+ static get targets () {
5
+ return ['button', 'popup']
6
+ }
7
+
8
+ // Attaches controller logic to the element itself
9
+ // This allows calling controller methods from the element in other controllers
10
+ connect () {
11
+ this.element.controller = this
12
+
13
+ // Clicked outside popup
14
+ document.addEventListener('click', (event) => {
15
+ this.handleOutsideClick(event)
16
+ })
17
+ }
18
+
19
+ handleOutsideClick (event) {
20
+ if (!this.isClickedInside(event)) {
21
+ this.close()
6
22
  }
7
-
8
- // Attaches controller logic to the element itself
9
- // This allows calling controller methods from the element in other controllers
10
- connect() {
11
- this.element['controller'] = this
12
-
13
- // Clicked outside popup
14
- document.addEventListener('click', (event) => {
15
- this.handleOutsideClick(event)
16
- })
17
- }
18
-
19
- handleOutsideClick(event) {
20
- if (!this.isClickedInside(event)) {
21
- this.close()
22
- }
23
+ }
24
+
25
+ toggle (event) {
26
+ const expanded = this.buttonTarget.getAttribute('aria-expanded') === 'true'
27
+ if (expanded) {
28
+ this.close(null)
29
+ } else {
30
+ this.open()
23
31
  }
32
+ }
24
33
 
25
- toggle(event) {
26
- const expanded = this.buttonTarget.getAttribute('aria-expanded') === 'true'
27
- if (expanded) {
28
- this.close(null)
29
- } else {
30
- this.open()
31
- }
32
- }
33
-
34
- open() {
35
- this.buttonTarget.setAttribute('aria-expanded', 'true')
36
- this.popupTarget.classList.remove('closed')
37
- }
34
+ open () {
35
+ this.buttonTarget.setAttribute('aria-expanded', 'true')
36
+ this.popupTarget.classList.remove('closed')
37
+ }
38
38
 
39
- close(event) {
40
- this.buttonTarget.setAttribute('aria-expanded', 'false')
41
- this.popupTarget.classList.add('closed')
42
- }
39
+ close (event) {
40
+ this.buttonTarget.setAttribute('aria-expanded', 'false')
41
+ this.popupTarget.classList.add('closed')
42
+ }
43
43
 
44
- isClickedInside(event) {
45
- if (!event) {
46
- return false
47
- }
48
- const inPopup = this.popupTarget.contains(event.target)
49
- const inButton = this.buttonTarget.contains(event.target)
50
- const inAddButton = event.target.dataset.action === "click->filters#add"
51
- return (inPopup || inButton || inAddButton)
44
+ isClickedInside (event) {
45
+ if (!event) {
46
+ return false
52
47
  }
48
+ const inPopup = this.popupTarget.contains(event.target)
49
+ const inButton = this.buttonTarget.contains(event.target)
50
+ const inAddButton = event.target.dataset.action === 'click->filters#add'
51
+ return (inPopup || inButton || inAddButton)
52
+ }
53
53
  }