headmin 0.5.1 → 0.5.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 (142) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +2 -2
  3. data/Gemfile +14 -0
  4. data/Gemfile.lock +77 -0
  5. data/app/assets/javascripts/headmin/controllers/media_controller.js +237 -0
  6. data/app/assets/javascripts/headmin/controllers/media_modal_controller.js +110 -0
  7. data/app/assets/javascripts/headmin/controllers/remote_modal_controller.js +10 -0
  8. data/app/assets/javascripts/headmin/controllers/textarea_controller.js +34 -0
  9. data/app/assets/javascripts/headmin/index.js +8 -0
  10. data/app/assets/javascripts/headmin.js +294 -0
  11. data/app/assets/stylesheets/headmin/forms/file.scss +40 -5
  12. data/app/assets/stylesheets/headmin/forms/media.scss +10 -0
  13. data/app/assets/stylesheets/headmin/forms/repeater.scss +4 -0
  14. data/app/assets/stylesheets/headmin/forms.scss +7 -0
  15. data/app/assets/stylesheets/headmin/layout/sidebar.scss +0 -1
  16. data/app/assets/stylesheets/headmin/media/index.scss +9 -0
  17. data/app/assets/stylesheets/headmin/media.scss +1 -0
  18. data/app/assets/stylesheets/headmin/table.scss +8 -0
  19. data/app/assets/stylesheets/headmin.css +64 -7
  20. data/app/assets/stylesheets/headmin.scss +1 -0
  21. data/app/controllers/headmin/media_controller.rb +52 -0
  22. data/app/controllers/headmin_controller.rb +2 -0
  23. data/app/models/concerns/headmin/field.rb +0 -1
  24. data/app/models/concerns/headmin/fieldable.rb +10 -1
  25. data/app/models/concerns/headmin/form/hintable.rb +6 -1
  26. data/app/models/headmin/filter/date.rb +49 -1
  27. data/app/models/headmin/form/media_view.rb +113 -0
  28. data/app/models/headmin/form/textarea_view.rb +6 -1
  29. data/app/views/examples/admin.html.erb +13 -13
  30. data/app/views/examples/auth.html.erb +1 -1
  31. data/app/views/headmin/_blocks.html.erb +2 -2
  32. data/app/views/headmin/_breadcrumbs.html.erb +1 -1
  33. data/app/views/headmin/_dropdown.html.erb +1 -1
  34. data/app/views/headmin/_filters.html.erb +3 -3
  35. data/app/views/headmin/_form.html.erb +2 -2
  36. data/app/views/headmin/_heading.html.erb +1 -1
  37. data/app/views/headmin/_index.html.erb +1 -1
  38. data/app/views/headmin/_notifications.html.erb +1 -1
  39. data/app/views/headmin/_pagination.html.erb +1 -1
  40. data/app/views/headmin/_popup.html.erb +2 -2
  41. data/app/views/headmin/_table.html.erb +1 -1
  42. data/app/views/headmin/dropdown/_devise.html.erb +8 -8
  43. data/app/views/headmin/dropdown/_list.html.erb +1 -1
  44. data/app/views/headmin/dropdown/_locale.html.erb +4 -4
  45. data/app/views/headmin/filters/_base.html.erb +20 -20
  46. data/app/views/headmin/filters/_boolean.html.erb +4 -4
  47. data/app/views/headmin/filters/_date.html.erb +4 -4
  48. data/app/views/headmin/filters/_flatpickr.html.erb +4 -4
  49. data/app/views/headmin/filters/_number.html.erb +4 -4
  50. data/app/views/headmin/filters/_options.html.erb +4 -4
  51. data/app/views/headmin/filters/_search.html.erb +3 -3
  52. data/app/views/headmin/filters/_text.html.erb +4 -4
  53. data/app/views/headmin/filters/filter/_button.html.erb +1 -1
  54. data/app/views/headmin/forms/_autocomplete.html.erb +2 -2
  55. data/app/views/headmin/forms/_blocks.html.erb +4 -4
  56. data/app/views/headmin/forms/_checkbox.html.erb +5 -5
  57. data/app/views/headmin/forms/_color.html.erb +5 -5
  58. data/app/views/headmin/forms/_date.html.erb +8 -8
  59. data/app/views/headmin/forms/_date_range.html.erb +3 -3
  60. data/app/views/headmin/forms/_datetime.html.erb +8 -8
  61. data/app/views/headmin/forms/_datetime_range.html.erb +3 -3
  62. data/app/views/headmin/forms/_email.html.erb +9 -9
  63. data/app/views/headmin/forms/_file.html.erb +12 -12
  64. data/app/views/headmin/forms/_flatpickr.html.erb +2 -2
  65. data/app/views/headmin/forms/_flatpickr_range.html.erb +8 -8
  66. data/app/views/headmin/forms/_hidden.html.erb +1 -1
  67. data/app/views/headmin/forms/_hint.html.erb +7 -2
  68. data/app/views/headmin/forms/_label.html.erb +1 -1
  69. data/app/views/headmin/forms/_media.html.erb +58 -0
  70. data/app/views/headmin/forms/_number.html.erb +8 -8
  71. data/app/views/headmin/forms/_password.html.erb +7 -7
  72. data/app/views/headmin/forms/_redactorx.html.erb +2 -2
  73. data/app/views/headmin/forms/_repeater.html.erb +12 -12
  74. data/app/views/headmin/forms/_search.html.erb +9 -9
  75. data/app/views/headmin/forms/_select.html.erb +8 -8
  76. data/app/views/headmin/forms/_switch.html.erb +2 -2
  77. data/app/views/headmin/forms/_text.html.erb +9 -9
  78. data/app/views/headmin/forms/_textarea.html.erb +7 -7
  79. data/app/views/headmin/forms/_url.html.erb +9 -9
  80. data/app/views/headmin/forms/_validation.html.erb +1 -1
  81. data/app/views/headmin/forms/_wysiwyg.html.erb +2 -2
  82. data/app/views/headmin/forms/fields/_base.html.erb +2 -2
  83. data/app/views/headmin/forms/fields/_file.html.erb +3 -3
  84. data/app/views/headmin/forms/fields/_files.html.erb +3 -3
  85. data/app/views/headmin/forms/fields/_group.html.erb +7 -7
  86. data/app/views/headmin/forms/fields/_list.html.erb +5 -5
  87. data/app/views/headmin/forms/fields/_text.html.erb +3 -3
  88. data/app/views/headmin/forms/media/_item.html.erb +32 -0
  89. data/app/views/headmin/forms/media/_validation.html.erb +10 -0
  90. data/app/views/headmin/forms/repeater/_row.html.erb +12 -11
  91. data/app/views/headmin/layout/_main.html.erb +3 -1
  92. data/app/views/headmin/layout/_remote_modal.html.erb +1 -0
  93. data/app/views/headmin/layout/_sidebar.html.erb +1 -1
  94. data/app/views/headmin/media/_item.html.erb +17 -0
  95. data/app/views/headmin/media/_media_item_modal.html.erb +51 -0
  96. data/app/views/headmin/media/_modal.html.erb +35 -0
  97. data/app/views/headmin/media/create.turbo_stream.erb +5 -0
  98. data/app/views/headmin/media/index.html.erb +3 -0
  99. data/app/views/headmin/media/show.html.erb +9 -0
  100. data/app/views/headmin/media/update.turbo_stream.erb +3 -0
  101. data/app/views/headmin/nav/_dropdown.html.erb +3 -3
  102. data/app/views/headmin/nav/_item.html.erb +2 -2
  103. data/app/views/headmin/nav/item/_devise.html.erb +6 -6
  104. data/app/views/headmin/nav/item/_locale.html.erb +4 -4
  105. data/app/views/headmin/table/_actions.html.erb +2 -2
  106. data/app/views/headmin/table/actions/_action.html.erb +3 -3
  107. data/app/views/headmin/table/actions/_delete.html.erb +2 -2
  108. data/app/views/headmin/table/actions/_export.html.erb +2 -2
  109. data/app/views/headmin/table/body/_image.html.erb +18 -0
  110. data/app/views/headmin/table/body/_row.html.erb +3 -3
  111. data/app/views/headmin/table/foot/_cell.html.erb +1 -1
  112. data/app/views/headmin/views/devise/confirmations/_new.html.erb +3 -3
  113. data/app/views/headmin/views/devise/passwords/_edit.html.erb +4 -4
  114. data/app/views/headmin/views/devise/passwords/_new.html.erb +3 -3
  115. data/app/views/headmin/views/devise/registrations/_edit.html.erb +5 -5
  116. data/app/views/headmin/views/devise/registrations/_new.html.erb +5 -5
  117. data/app/views/headmin/views/devise/sessions/_new.html.erb +4 -4
  118. data/app/views/headmin/views/devise/unlocks/_new.html.erb +3 -3
  119. data/config/locales/devise/nl.yml +1 -1
  120. data/config/locales/headmin/forms/en.yml +8 -0
  121. data/config/locales/headmin/forms/nl.yml +8 -0
  122. data/config/locales/headmin/media/en.yml +23 -0
  123. data/config/locales/headmin/media/nl.yml +22 -0
  124. data/config/locales/headmin/table/en.yml +2 -0
  125. data/config/locales/headmin/table/nl.yml +2 -0
  126. data/config/routes.rb +10 -0
  127. data/lib/generators/templates/views/auth/confirmations/new.html.erb +1 -1
  128. data/lib/generators/templates/views/auth/mailer/confirmation_instructions.html.erb +1 -1
  129. data/lib/generators/templates/views/auth/mailer/email_changed.html.erb +1 -1
  130. data/lib/generators/templates/views/auth/mailer/password_change.html.erb +1 -1
  131. data/lib/generators/templates/views/auth/mailer/reset_password_instructions.html.erb +1 -1
  132. data/lib/generators/templates/views/auth/mailer/unlock_instructions.html.erb +1 -1
  133. data/lib/generators/templates/views/auth/passwords/edit.html.erb +1 -1
  134. data/lib/generators/templates/views/auth/passwords/new.html.erb +1 -1
  135. data/lib/generators/templates/views/auth/registrations/edit.html.erb +1 -1
  136. data/lib/generators/templates/views/auth/registrations/new.html.erb +1 -1
  137. data/lib/generators/templates/views/auth/sessions/new.html.erb +1 -1
  138. data/lib/generators/templates/views/auth/unlocks/new.html.erb +1 -1
  139. data/lib/generators/templates/views/layouts/auth.html.erb +1 -1
  140. data/lib/headmin/version.rb +1 -1
  141. metadata +27 -3
  142. data/.lock-487e157d270f3062a98b7b2a012753708-1272821827 +0 -0
@@ -10093,6 +10093,260 @@ var hello_controller_default = class extends Controller {
10093
10093
  }
10094
10094
  };
10095
10095
 
10096
+ // app/assets/javascripts/headmin/controllers/media_controller.js
10097
+ var media_controller_default = class extends Controller {
10098
+ static get targets() {
10099
+ return ["item", "template", "thumbnails", "modalButton", "placeholder", "validationInput", "count", "editButton"];
10100
+ }
10101
+ connect() {
10102
+ document.addEventListener("mediaSelectionSubmitted", (event) => {
10103
+ if (event.detail.name === this.element.dataset.name) {
10104
+ this.selectItems(event.detail.items);
10105
+ }
10106
+ });
10107
+ if (this.hasSorting()) {
10108
+ this.initSortable();
10109
+ }
10110
+ this.validate();
10111
+ }
10112
+ destroy(event) {
10113
+ const item = event.currentTarget.closest("[data-media-target='item']");
10114
+ this.destroyItem(item);
10115
+ }
10116
+ syncIds() {
10117
+ const ids = this.activeIds();
10118
+ this.modalButtonTargets.forEach((button) => {
10119
+ const url = new URL(button.getAttribute("href"));
10120
+ url.searchParams.delete("ids[]");
10121
+ ids.forEach((id) => {
10122
+ url.searchParams.append("ids[]", id);
10123
+ });
10124
+ button.setAttribute("href", url.toString());
10125
+ });
10126
+ }
10127
+ initSortable() {
10128
+ sortable_esm_default.create(this.thumbnailsTarget, {
10129
+ handle: ".media-drag-sort-handle",
10130
+ onEnd: (event) => {
10131
+ this.resetPositions();
10132
+ }
10133
+ });
10134
+ }
10135
+ hasSorting() {
10136
+ return this.element.dataset.sort === "true";
10137
+ }
10138
+ destroyItem(item) {
10139
+ this.removeItem(item);
10140
+ this.postProcess();
10141
+ }
10142
+ selectItems(items) {
10143
+ this.removeAllItems();
10144
+ this.addItems(items);
10145
+ this.postProcess();
10146
+ }
10147
+ postProcess() {
10148
+ this.resetPositions();
10149
+ this.syncIds();
10150
+ this.togglePlaceholder();
10151
+ this.validate();
10152
+ }
10153
+ validate() {
10154
+ this.clearValidation();
10155
+ if (this.element.dataset.required === "0")
10156
+ return;
10157
+ this.validateMinimum();
10158
+ this.validateMaximum();
10159
+ }
10160
+ clearValidation() {
10161
+ this.validationInputTarget.setCustomValidity("");
10162
+ }
10163
+ validateMinimum() {
10164
+ const count = this.activeItems().length;
10165
+ if (count < this.minActiveItems()) {
10166
+ this.validationInputTarget.setCustomValidity(this.validationInputTarget.dataset.minMessage);
10167
+ }
10168
+ }
10169
+ validateMaximum() {
10170
+ const count = this.activeItems().length;
10171
+ if (count > this.maxActiveItems()) {
10172
+ this.validationInputTarget.setCustomValidity(this.validationInputTarget.dataset.maxMessage);
10173
+ }
10174
+ }
10175
+ minActiveItems() {
10176
+ return parseInt(this.element.dataset.min, 10) || 0;
10177
+ }
10178
+ maxActiveItems() {
10179
+ return parseInt(this.element.dataset.max, 10) || Infinity;
10180
+ }
10181
+ resetPositions() {
10182
+ this.activeItems().forEach((item, index2) => {
10183
+ const positionInput = item.querySelector("input[name*='position']");
10184
+ if (positionInput) {
10185
+ positionInput.value = index2;
10186
+ }
10187
+ });
10188
+ }
10189
+ addItems(items) {
10190
+ items.forEach((item) => this.addItem(item));
10191
+ }
10192
+ addItem(item) {
10193
+ const currentItem = this.itemByBlobId(item.blobId);
10194
+ if (currentItem) {
10195
+ this.enableItem(currentItem);
10196
+ } else {
10197
+ this.createItem(item);
10198
+ }
10199
+ }
10200
+ togglePlaceholder() {
10201
+ if (this.activeItems().length > 0) {
10202
+ this.hidePlaceholder();
10203
+ } else {
10204
+ this.showPlaceholder();
10205
+ }
10206
+ }
10207
+ showPlaceholder() {
10208
+ this.placeholderTarget.classList.remove("d-none");
10209
+ }
10210
+ hidePlaceholder() {
10211
+ this.placeholderTarget.classList.add("d-none");
10212
+ }
10213
+ enableItem(item) {
10214
+ item.querySelector(`input[name*='_destroy']`).value = false;
10215
+ item.classList.remove("d-none");
10216
+ }
10217
+ createItem(item) {
10218
+ const template = this.templateTarget;
10219
+ const html = this.randomizeIds(template);
10220
+ this.thumbnailsTarget.insertAdjacentHTML("beforeend", html);
10221
+ const newItem = this.itemTargets.pop();
10222
+ newItem.querySelector(`input[name*="[blob_id]"]`).value = item.blobId;
10223
+ newItem.querySelector(`input[name*="[_destroy]"]`).value = false;
10224
+ const editButton = newItem.querySelector(`[data-media-target="editButton"]`);
10225
+ editButton.setAttribute("href", editButton.getAttribute("href").replace("$1", item.blobId));
10226
+ const oldThumbnail = newItem.querySelector(".h-thumbnail");
10227
+ const newThumbnail = item.thumbnail.cloneNode(true);
10228
+ oldThumbnail.parentNode.replaceChild(newThumbnail, oldThumbnail);
10229
+ }
10230
+ randomizeIds(template) {
10231
+ const regex = new RegExp(template.dataset.templateIdRegex, "g");
10232
+ const randomNumber = Math.floor(1e8 + Math.random() * 9e8);
10233
+ return template.innerHTML.replace(regex, randomNumber);
10234
+ }
10235
+ removeAllItems() {
10236
+ this.removeItems(this.itemTargets);
10237
+ }
10238
+ removeItems(items) {
10239
+ items.forEach((item) => {
10240
+ this.removeItem(item);
10241
+ });
10242
+ }
10243
+ removeItem(item) {
10244
+ item.querySelector(`input[name*='_destroy']`).value = 1;
10245
+ item.classList.add("d-none");
10246
+ this.resetPositions();
10247
+ this.syncIds();
10248
+ this.togglePlaceholder();
10249
+ }
10250
+ itemByBlobId(blobId) {
10251
+ return this.itemTargets.find((item) => {
10252
+ return item.querySelector(`input[name*='blob_id']`).value === blobId;
10253
+ });
10254
+ }
10255
+ activeItems() {
10256
+ return this.itemTargets.filter((item) => {
10257
+ return item.querySelector(`input[name$='[_destroy]']`).value === "false";
10258
+ });
10259
+ }
10260
+ activeIds() {
10261
+ return this.activeItems().map((item) => {
10262
+ return item.querySelector(`input[name$='[blob_id]']`).value;
10263
+ });
10264
+ }
10265
+ };
10266
+
10267
+ // app/assets/javascripts/headmin/controllers/media_modal_controller.js
10268
+ var media_modal_controller_default = class extends Controller {
10269
+ static get targets() {
10270
+ return ["idCheckbox", "item", "form", "selectButton", "placeholder", "count"];
10271
+ }
10272
+ connect() {
10273
+ this.validate();
10274
+ this.updateCount();
10275
+ }
10276
+ select() {
10277
+ this.dispatchSelectionEvent();
10278
+ }
10279
+ submitForm() {
10280
+ this.hidePlaceholder();
10281
+ this.triggerFormSubmission();
10282
+ }
10283
+ inputChange() {
10284
+ this.handleInputChange();
10285
+ this.updateCount();
10286
+ }
10287
+ hidePlaceholder() {
10288
+ this.placeholderTarget.classList.add("d-none");
10289
+ }
10290
+ handleInputChange() {
10291
+ this.validate();
10292
+ }
10293
+ dispatchSelectionEvent() {
10294
+ document.dispatchEvent(new CustomEvent("mediaSelectionSubmitted", {
10295
+ detail: {
10296
+ name: this.element.dataset.name,
10297
+ items: this.renderItemsForEvent(this.selectedItems())
10298
+ }
10299
+ }));
10300
+ }
10301
+ triggerFormSubmission() {
10302
+ this.formTarget.requestSubmit();
10303
+ }
10304
+ renderItemsForEvent(items) {
10305
+ return items.map((item) => this.renderItemForEvent(item));
10306
+ }
10307
+ renderItemForEvent(item) {
10308
+ return {
10309
+ blobId: item.querySelector('input[type="checkbox"]').value,
10310
+ thumbnail: item.querySelector(".h-thumbnail")
10311
+ };
10312
+ }
10313
+ selectedItems() {
10314
+ return this.itemTargets.filter((item) => {
10315
+ const checkbox = item.querySelector('input[type="checkbox"]');
10316
+ return checkbox.checked;
10317
+ });
10318
+ }
10319
+ selectedItemsCount() {
10320
+ return this.selectedItems().length;
10321
+ }
10322
+ minSelectedItems() {
10323
+ return parseInt(this.element.dataset.min, 10) || 0;
10324
+ }
10325
+ maxSelectedItems() {
10326
+ return parseInt(this.element.dataset.max, 10) || Infinity;
10327
+ }
10328
+ validate() {
10329
+ if (this.isValid()) {
10330
+ this.enableSelectButton();
10331
+ } else {
10332
+ this.disableSelectButton();
10333
+ }
10334
+ }
10335
+ enableSelectButton() {
10336
+ this.selectButtonTarget.removeAttribute("disabled");
10337
+ }
10338
+ disableSelectButton() {
10339
+ this.selectButtonTarget.setAttribute("disabled", "");
10340
+ }
10341
+ isValid() {
10342
+ const count = this.selectedItemsCount();
10343
+ return count >= this.minSelectedItems() && count <= this.maxSelectedItems();
10344
+ }
10345
+ updateCount() {
10346
+ this.countTarget.innerHTML = this.idCheckboxTargets.filter((checkbox) => checkbox.checked).length;
10347
+ }
10348
+ };
10349
+
10096
10350
  // node_modules/@popperjs/core/lib/index.js
10097
10351
  var lib_exports = {};
10098
10352
  __export(lib_exports, {
@@ -15236,6 +15490,14 @@ var redactorx_controller_default = class extends Controller {
15236
15490
  }
15237
15491
  };
15238
15492
 
15493
+ // app/assets/javascripts/headmin/controllers/remote_modal_controller.js
15494
+ var remote_modal_controller_default = class extends Controller {
15495
+ connect() {
15496
+ this.modal = new Modal(this.element);
15497
+ this.modal.show();
15498
+ }
15499
+ };
15500
+
15239
15501
  // app/assets/javascripts/headmin/controllers/repeater_controller.js
15240
15502
  var repeater_controller_default = class extends Controller {
15241
15503
  static get values() {
@@ -15580,6 +15842,34 @@ var table_controller_default = class extends Controller {
15580
15842
  }
15581
15843
  };
15582
15844
 
15845
+ // app/assets/javascripts/headmin/controllers/textarea_controller.js
15846
+ var textarea_controller_default = class extends Controller {
15847
+ static get targets() {
15848
+ return ["textarea", "count"];
15849
+ }
15850
+ connect() {
15851
+ this.update();
15852
+ }
15853
+ update() {
15854
+ this.resize();
15855
+ this.updateCount();
15856
+ }
15857
+ resize() {
15858
+ this.textareaTarget.style.height = "auto";
15859
+ this.textareaTarget.setAttribute("style", "height:" + this.textareaTarget.scrollHeight + "px;overflow-y:hidden;");
15860
+ }
15861
+ updateCount() {
15862
+ if (this.textareaTarget.getAttribute("maxlength")) {
15863
+ this.updateCountLength();
15864
+ }
15865
+ }
15866
+ updateCountLength() {
15867
+ const current_length = this.textareaTarget.value.length;
15868
+ const maximum_length = this.textareaTarget.getAttribute("maxlength");
15869
+ this.countTarget.textContent = `${current_length}/${maximum_length}`;
15870
+ }
15871
+ };
15872
+
15583
15873
  // app/assets/javascripts/headmin/index.js
15584
15874
  var Headmin = class {
15585
15875
  static start() {
@@ -15594,13 +15884,17 @@ var Headmin = class {
15594
15884
  Stimulus.register("filters", filters_controller_default);
15595
15885
  Stimulus.register("flatpickr", flatpickr_controller_default);
15596
15886
  Stimulus.register("hello", hello_controller_default);
15887
+ Stimulus.register("media", media_controller_default);
15888
+ Stimulus.register("media-modal", media_modal_controller_default);
15597
15889
  Stimulus.register("notification", notification_controller_default);
15598
15890
  Stimulus.register("popup", popup_controller_default);
15599
15891
  Stimulus.register("redactorx", redactorx_controller_default);
15892
+ Stimulus.register("remote-modal", remote_modal_controller_default);
15600
15893
  Stimulus.register("repeater", repeater_controller_default);
15601
15894
  Stimulus.register("select", select_controller_default);
15602
15895
  Stimulus.register("table", table_controller_default);
15603
15896
  Stimulus.register("table-actions", table_actions_controller_default);
15897
+ Stimulus.register("textarea", textarea_controller_default);
15604
15898
  }
15605
15899
  };
15606
15900
  export {
@@ -17,26 +17,41 @@
17
17
  display: flex;
18
18
  flex-wrap: wrap;
19
19
  gap: map-get($spacers, 2);
20
+
21
+ a, a:link, a:hover {
22
+ color: inherit;
23
+ text-decoration: inherit;
24
+ }
20
25
  }
21
26
 
22
27
  .h-form-file-thumbnail {
23
28
  position: relative;
24
29
  pointer-events: initial !important;
25
30
  display: flex;
31
+
32
+ &:hover {
33
+ .h-form-file-thumbnail-actions {
34
+ display: block;
35
+ }
36
+ }
37
+ }
38
+
39
+ .h-form-file-thumbnail-actions {
40
+ display: none;
26
41
  }
27
42
 
28
43
  .h-form-file-thumbnail-remove {
29
44
  position: absolute;
30
- top: 0;
31
- right: 0;
45
+ top: 5px;
46
+ right: 5px;
32
47
  background: $danger;
33
- width: 20px;
34
- height: 20px;
48
+ width: 25px;
49
+ height: 25px;
35
50
  display: flex;
36
51
  align-items: center;
37
52
  justify-content: center;
38
53
  color: white;
39
- border-radius: $border-radius;
54
+ border-radius: $border-radius-pill;
40
55
  z-index: 3;
41
56
  cursor: pointer;
42
57
 
@@ -44,3 +59,23 @@
44
59
  background: tint-color($danger, $btn-hover-bg-tint-amount)
45
60
  }
46
61
  }
62
+
63
+ .h-form-file-thumbnail-edit {
64
+ position: absolute;
65
+ top: 5px;
66
+ right: 35px;
67
+ background-color: rgba(0, 0, 0, 0.7);
68
+ width: 25px;
69
+ height: 25px;
70
+ display: flex;
71
+ align-items: center;
72
+ justify-content: center;
73
+ border-radius: $border-radius-pill;
74
+ z-index: 3;
75
+ cursor: pointer;
76
+ color: white !important;
77
+
78
+ &:hover {
79
+ background-color: rgba(0, 0, 0, 1);
80
+ }
81
+ }
@@ -0,0 +1,10 @@
1
+ .h-form-media-validation {
2
+ opacity: 0;
3
+ width: 100px;
4
+ height: 0;
5
+ padding: 0;
6
+ margin: 0;
7
+ position: absolute;
8
+ bottom: 0;
9
+ left: 0;
10
+ }
@@ -18,6 +18,10 @@
18
18
  visibility: visible;
19
19
  }
20
20
  }
21
+
22
+ .mb-3:last-of-type {
23
+ margin-bottom: 0 !important;
24
+ }
21
25
  }
22
26
 
23
27
  .repeater-row-remove {
@@ -5,7 +5,14 @@
5
5
  }
6
6
  }
7
7
 
8
+ .card-body {
9
+ .mb-3:last-child {
10
+ margin-bottom: 0 !important;
11
+ }
12
+ }
13
+
8
14
  @import "forms/autocomplete";
9
15
  @import "forms/file";
16
+ @import "forms/media";
10
17
  @import "forms/repeater";
11
18
  @import "forms/search";
@@ -14,7 +14,6 @@
14
14
  .nav-brand {
15
15
  svg, img {
16
16
  max-width: 100%;
17
- height: 30px;
18
17
  }
19
18
  svg {
20
19
  fill: $white;
@@ -0,0 +1,9 @@
1
+ .media-modal {
2
+ .h-thumbnail {
3
+ cursor: pointer;
4
+ }
5
+
6
+ input[type="checkbox"]:checked + label .h-thumbnail {
7
+ background-color: #0d6efd;
8
+ }
9
+ }
@@ -0,0 +1 @@
1
+ @import "media/index";
@@ -35,6 +35,14 @@
35
35
  }
36
36
  }
37
37
 
38
+ .h-table-cell-image {
39
+ padding: 0.4rem 1rem !important;
40
+
41
+ img {
42
+ border-radius: 3px;
43
+ }
44
+ }
45
+
38
46
  .h-table-cell-color {
39
47
  width: 22px;
40
48
  height: 22px;
@@ -1,7 +1,7 @@
1
1
  @charset "UTF-8";
2
2
  @import "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css";
3
3
 
4
- /* sass-plugin-0:/opt/homebrew/var/www/headmin/src/scss/headmin.scss */
4
+ /* sass-plugin-0:/usr/local/var/www/headmin/src/scss/headmin.scss */
5
5
  :root {
6
6
  --bs-blue: #0d6efd;
7
7
  --bs-indigo: #6610f2;
@@ -12971,6 +12971,9 @@ span.flatpickr-weekday {
12971
12971
  content: " *";
12972
12972
  color: #dc3545;
12973
12973
  }
12974
+ .card-body .mb-3:last-child {
12975
+ margin-bottom: 0 !important;
12976
+ }
12974
12977
  .h-autocomplete {
12975
12978
  position: absolute;
12976
12979
  top: calc(100% + 2px);
@@ -13019,29 +13022,69 @@ span.flatpickr-weekday {
13019
13022
  flex-wrap: wrap;
13020
13023
  gap: 0.5rem;
13021
13024
  }
13025
+ .h-form-file-thumbnails a,
13026
+ .h-form-file-thumbnails a:link,
13027
+ .h-form-file-thumbnails a:hover {
13028
+ color: inherit;
13029
+ text-decoration: inherit;
13030
+ }
13022
13031
  .h-form-file-thumbnail {
13023
13032
  position: relative;
13024
13033
  pointer-events: initial !important;
13025
13034
  display: flex;
13026
13035
  }
13036
+ .h-form-file-thumbnail:hover .h-form-file-thumbnail-actions {
13037
+ display: block;
13038
+ }
13039
+ .h-form-file-thumbnail-actions {
13040
+ display: none;
13041
+ }
13027
13042
  .h-form-file-thumbnail-remove {
13028
13043
  position: absolute;
13029
- top: 0;
13030
- right: 0;
13044
+ top: 5px;
13045
+ right: 5px;
13031
13046
  background: #dc3545;
13032
- width: 20px;
13033
- height: 20px;
13047
+ width: 25px;
13048
+ height: 25px;
13034
13049
  display: flex;
13035
13050
  align-items: center;
13036
13051
  justify-content: center;
13037
13052
  color: white;
13038
- border-radius: 0.25rem;
13053
+ border-radius: 50rem;
13039
13054
  z-index: 3;
13040
13055
  cursor: pointer;
13041
13056
  }
13042
13057
  .h-form-file-thumbnail-remove:hover {
13043
13058
  background: #e15361;
13044
13059
  }
13060
+ .h-form-file-thumbnail-edit {
13061
+ position: absolute;
13062
+ top: 5px;
13063
+ right: 35px;
13064
+ background-color: rgba(0, 0, 0, 0.7);
13065
+ width: 25px;
13066
+ height: 25px;
13067
+ display: flex;
13068
+ align-items: center;
13069
+ justify-content: center;
13070
+ border-radius: 50rem;
13071
+ z-index: 3;
13072
+ cursor: pointer;
13073
+ color: white !important;
13074
+ }
13075
+ .h-form-file-thumbnail-edit:hover {
13076
+ background-color: black;
13077
+ }
13078
+ .h-form-media-validation {
13079
+ opacity: 0;
13080
+ width: 100px;
13081
+ height: 0;
13082
+ padding: 0;
13083
+ margin: 0;
13084
+ position: absolute;
13085
+ bottom: 0;
13086
+ left: 0;
13087
+ }
13045
13088
  .repeater {
13046
13089
  position: relative;
13047
13090
  }
@@ -13057,6 +13100,9 @@ span.flatpickr-weekday {
13057
13100
  .repeater-row:hover .repeater-row-handle {
13058
13101
  visibility: visible;
13059
13102
  }
13103
+ .repeater-row .mb-3:last-of-type {
13104
+ margin-bottom: 0 !important;
13105
+ }
13060
13106
  .repeater-row-remove {
13061
13107
  position: absolute;
13062
13108
  top: calc(50% - 17px);
@@ -13138,7 +13184,6 @@ mark,
13138
13184
  .nav-brand svg,
13139
13185
  .nav-brand img {
13140
13186
  max-width: 100%;
13141
- height: 30px;
13142
13187
  }
13143
13188
  .nav-brand svg {
13144
13189
  fill: #ffffff;
@@ -13204,6 +13249,12 @@ body.empty {
13204
13249
  width: 300px;
13205
13250
  max-width: 100%;
13206
13251
  }
13252
+ .h-table-cell-image {
13253
+ padding: 0.4rem 1rem !important;
13254
+ }
13255
+ .h-table-cell-image img {
13256
+ border-radius: 3px;
13257
+ }
13207
13258
  .h-table-cell-color {
13208
13259
  width: 22px;
13209
13260
  height: 22px;
@@ -13390,6 +13441,12 @@ body.empty {
13390
13441
  z-index: 9;
13391
13442
  display: none;
13392
13443
  }
13444
+ .media-modal .h-thumbnail {
13445
+ cursor: pointer;
13446
+ }
13447
+ .media-modal input[type=checkbox]:checked + label .h-thumbnail {
13448
+ background-color: #0d6efd;
13449
+ }
13393
13450
  .h-popup {
13394
13451
  padding: 10px;
13395
13452
  background: #ffffff;
@@ -60,6 +60,7 @@
60
60
  @import "headmin/table";
61
61
  @import "headmin/utilities";
62
62
  @import "headmin/filter";
63
+ @import "headmin/media";
63
64
  @import "headmin/popup";
64
65
  @import "headmin/thumbnail";
65
66
  @import "headmin/syntax";
@@ -0,0 +1,52 @@
1
+ class Headmin::MediaController < HeadminController
2
+ layout false
3
+
4
+ def index
5
+ @blobs =
6
+ ActiveStorage::Blob
7
+ .left_outer_joins(:attachments)
8
+ .where.not(active_storage_attachments: {record_type: "ActiveStorage::VariantRecord"}) # Not a variant
9
+ .or(ActiveStorage::Blob.where(active_storage_attachments: {id: nil})) # Or an orphan
10
+ .order(created_at: :desc)
11
+ .group(:id)
12
+ .all
13
+ end
14
+
15
+ def create
16
+ blobs = []
17
+ media_params[:files].reject { |c| c.blank? }.each do |file|
18
+ blobs << ActiveStorage::Blob.create_and_upload!(io: file, filename: file.original_filename)
19
+ end
20
+
21
+ respond_to do |format|
22
+ format.turbo_stream {
23
+ @blobs = blobs
24
+ }
25
+ format.html { redirect_to root_path }
26
+ end
27
+ end
28
+
29
+ def show
30
+ @blob = ActiveStorage::Blob.find(params[:id])
31
+ end
32
+
33
+ def update
34
+ @blob = ActiveStorage::Blob.find(params[:id])
35
+ media_item_params[:filename] = media_item_params[:filename] + "." + @blob.filename.to_s.rpartition(".").last
36
+ if @blob.update(media_item_params)
37
+ flash.now[:notice] = t("admin.flash.updated", name: @blob.filename)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def media_params
44
+ params.permit(
45
+ files: []
46
+ )
47
+ end
48
+
49
+ def media_item_params
50
+ params.require(:blob).permit!
51
+ end
52
+ end
@@ -0,0 +1,2 @@
1
+ class HeadminController < ApplicationController
2
+ end
@@ -5,7 +5,6 @@ module Headmin
5
5
  included do
6
6
  # Configuration
7
7
  has_closure_tree
8
-
9
8
  # Associations
10
9
  belongs_to :fieldable, polymorphic: true, optional: true, touch: true
11
10
  belongs_to :field, optional: true, touch: true