b4um_generators 0.2.7 → 0.2.9

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ab89fe09deda48a796523b69839e38cacce98d2d91e417b0de179c3263b45be2
4
- data.tar.gz: b7c7173b4d447cd2a068fd84c06f268b4cdfd0eabfdd76115a6fdb4342b03a48
3
+ metadata.gz: 4554ba68c9377451369455eee637d0ebf9b05926d17f1cca4e7846ed9a3e7ffe
4
+ data.tar.gz: 52c056de9b177de1f0e790ede1d7c13fcd5888c18eb95e02d5e93895c2827040
5
5
  SHA512:
6
- metadata.gz: 38240797ad404a9ed5092ce1222b4e02a07bed73a217d16d72d2b3dbf22beafc29e3667031bd06a732b3ab5fa22d81ee1cf5ee670ce1d380868ec5364d93aea1
7
- data.tar.gz: 982dc00957e65b8f38233184369a8cfbce9ad00236fef09f750f029f22e4b64926e1ad194e4d785ab5a492c8ff197930eb62cccce766c882714d9275640b3971
6
+ metadata.gz: c2758a16ff6d6fdfc220c7c49c8c698d09628ba53ecf45054f446bf062d291dfed85c5ca2a31d50e69e3d6a77521500d01805eaa21ff07cc8bf1adabe6ec0566
7
+ data.tar.gz: 22027e9c4911cbc9b16f8ad15132ff9681c32eaa4b53760c4e7c5838f7ff86e609879d94450bf19e975a105a48c196864d7c5bdcbea351266901c109563399e8
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module B4umGenerators
4
- VERSION = "0.2.7"
4
+ VERSION = "0.2.9"
5
5
  end
@@ -182,18 +182,32 @@ module B4um
182
182
 
183
183
  navigation = File.read(full_navigation_path)
184
184
 
185
- return if navigation.include?(
185
+ if navigation.include?(
186
186
  'navigation_button_to "Logout", logout_path, method: :delete'
187
187
  )
188
+ add_register_navigation_link(
189
+ navigation_path,
190
+ navigation
191
+ )
192
+ return
193
+ end
188
194
 
189
195
  marker = "<%# B4UM_NAVIGATION_LINKS %>"
190
196
 
191
197
  return unless navigation.include?(marker)
192
198
 
199
+ controller_name = authentication_model_name.pluralize
200
+ new_resource_path =
201
+ "new_#{authentication_model_name}_path"
202
+
193
203
  authentication_links = <<~ERB
194
204
  <% if logged_in? %>
195
205
  <%= navigation_button_to "Logout", logout_path, method: :delete %>
196
206
  <% else %>
207
+ <%= navigation_link_to "Register",
208
+ #{new_resource_path},
209
+ controller: :#{controller_name},
210
+ action: :new %>
197
211
  <%= navigation_link_to "Login",
198
212
  login_path,
199
213
  controller: :sessions,
@@ -210,6 +224,106 @@ module B4um
210
224
  )
211
225
  end
212
226
 
227
+ def protect_authentication_resource
228
+ controller_path = authentication_resource_controller_path
229
+ full_controller_path = File.join(
230
+ destination_root,
231
+ controller_path
232
+ )
233
+
234
+ return unless File.exist?(full_controller_path)
235
+
236
+ add_authentication_resource_callbacks(controller_path)
237
+ add_authentication_resource_authorization(controller_path)
238
+ redirect_destroyed_authentication_resource_to_root(controller_path)
239
+ end
240
+
241
+ def sign_in_after_registration
242
+ controller_path = authentication_resource_controller_path
243
+
244
+ full_controller_path = File.join(
245
+ destination_root,
246
+ controller_path
247
+ )
248
+
249
+ return unless File.exist?(full_controller_path)
250
+
251
+ controller = File.read(full_controller_path)
252
+
253
+ session_assignment =
254
+ "session[:#{authentication_model_name}_id] = " \
255
+ "@#{authentication_model_name}.id"
256
+
257
+ return if controller.include?(session_assignment)
258
+
259
+ save_pattern =
260
+ /^(\s*)if @#{Regexp.escape(authentication_model_name)}\.save\s*$/
261
+
262
+ return unless controller.match?(save_pattern)
263
+
264
+ inject_into_file(
265
+ controller_path,
266
+ after: save_pattern
267
+ ) do
268
+ "\n\\1 #{session_assignment}"
269
+ end
270
+ end
271
+
272
+ def add_profile_navigation
273
+ navigation_path = "app/views/shared/_navigation.html.erb"
274
+
275
+ full_navigation_path = File.join(
276
+ destination_root,
277
+ navigation_path
278
+ )
279
+
280
+ return unless File.exist?(full_navigation_path)
281
+
282
+ navigation = File.read(full_navigation_path)
283
+
284
+ resource_name = authentication_class_name.pluralize
285
+ controller_name = authentication_model_name.pluralize
286
+
287
+ resource_pattern = /
288
+ ^[ \t]*<%=\s*navigation_link_to\s+"#{Regexp.escape(resource_name)}",
289
+ .*?
290
+ controller:\s*:#{Regexp.escape(controller_name)}
291
+ \s*%>\s*
292
+ /mx
293
+
294
+ if navigation.match?(resource_pattern)
295
+ gsub_file(
296
+ navigation_path,
297
+ resource_pattern,
298
+ ""
299
+ )
300
+ end
301
+
302
+ navigation = File.read(full_navigation_path)
303
+
304
+ return if navigation.include?(
305
+ 'navigation_link_to "Profile"'
306
+ )
307
+
308
+ login_marker = "<% if logged_in? %>"
309
+
310
+ return unless navigation.include?(login_marker)
311
+
312
+ profile_link = <<~ERB
313
+ <% if logged_in? %>
314
+ <%= navigation_link_to "Profile",
315
+ #{current_authentication_method},
316
+ controller: :#{controller_name},
317
+ action: :show %>
318
+ ERB
319
+
320
+ gsub_file(
321
+ navigation_path,
322
+ login_marker,
323
+ profile_link.chomp
324
+ )
325
+ end
326
+
213
327
  def protect_controllers
214
328
  protected_controller_names.each do |controller_name|
215
329
  controller_path = File.join(
@@ -251,6 +365,247 @@ module B4um
251
365
 
252
366
  private
253
367
 
368
+ def add_register_navigation_link(navigation_path, navigation)
369
+ return if navigation.include?(
370
+ 'navigation_link_to "Register",'
371
+ )
372
+
373
+ controller_name = authentication_model_name.pluralize
374
+ new_resource_path =
375
+ "new_#{authentication_model_name}_path"
376
+
377
+ login_pattern = /
378
+ ^([ \t]*)<%=\s*navigation_link_to\s+"Login",
379
+ /x
380
+
381
+ return unless navigation.match?(login_pattern)
382
+
383
+ register_link = <<~ERB
384
+ <%= navigation_link_to "Register",
385
+ #{new_resource_path},
386
+ controller: :#{controller_name},
387
+ action: :new %>
388
+ ERB
389
+
390
+ gsub_file(
391
+ navigation_path,
392
+ login_pattern
393
+ ) do |match|
394
+ indentation = match[/\A[ \t]*/]
395
+
396
+ indented_register = register_link.lines.map do |line|
397
+ "#{indentation}#{line}"
398
+ end.join
399
+
400
+ "#{indented_register}#{match}"
401
+ end
402
+ end
403
+
404
+ def authentication_resource_controller_path
405
+ File.join(
406
+ "app/controllers",
407
+ "#{authentication_model_name.pluralize}_controller.rb"
408
+ )
409
+ end
410
+
411
+ def redirect_destroyed_authentication_resource_to_root(controller_path)
412
+ resource_path =
413
+ "#{authentication_model_name.pluralize}_path"
414
+
415
+ redirect =
416
+ "redirect_to #{resource_path}, status: :see_other"
417
+
418
+ return unless controller_contains?(controller_path, redirect)
419
+
420
+ gsub_file(
421
+ controller_path,
422
+ redirect,
423
+ "redirect_to root_path, status: :see_other"
424
+ )
425
+ end
426
+
427
+ def add_authentication_resource_callbacks(controller_path)
428
+ add_require_login_callback(controller_path)
429
+ add_prevent_authentication_index_callback(controller_path)
430
+ add_require_current_authentication_callback(controller_path)
431
+ add_clear_authentication_session_callback(controller_path)
432
+ end
433
+
434
+ def add_require_login_callback(controller_path)
435
+ callback = "before_action :require_login, except: %i[ new create ]"
436
+
437
+ return if controller_contains?(controller_path, callback)
438
+
439
+ inject_into_file(
440
+ controller_path,
441
+ after: /^class .*Controller < ApplicationController\s*$/
442
+ ) do
443
+ "\n #{callback}"
444
+ end
445
+ end
446
+
447
+ def add_prevent_authentication_index_callback(controller_path)
448
+ callback =
449
+ "before_action :prevent_authentication_index, only: :index"
450
+
451
+ return if controller_contains?(controller_path, callback)
452
+
453
+ inject_into_file(
454
+ controller_path,
455
+ after: /^\s*before_action :require_login, except: %i\[ new create \]\s*$/
456
+ ) do
457
+ "\n #{callback}"
458
+ end
459
+ end
460
+
461
+ def add_require_current_authentication_callback(controller_path)
462
+ callback =
463
+ "before_action :require_current_#{authentication_model_name}, " \
464
+ "only: %i[ show edit update destroy ]"
465
+
466
+ return if controller_contains?(controller_path, callback)
467
+
468
+ set_callback =
469
+ /^\s*before_action :set_#{authentication_model_name}, only: %i\[ show edit update destroy \]\s*$/
470
+
471
+ if controller_matches?(controller_path, set_callback)
472
+ inject_into_file(
473
+ controller_path,
474
+ after: set_callback
475
+ ) do
476
+ "\n #{callback}"
477
+ end
478
+ else
479
+ inject_into_file(
480
+ controller_path,
481
+ after: /^class .*Controller < ApplicationController\s*$/
482
+ ) do
483
+ "\n #{callback}"
484
+ end
485
+ end
486
+ end
487
+
488
+ def add_clear_authentication_session_callback(controller_path)
489
+ callback =
490
+ "after_action :clear_authentication_session, only: :destroy"
491
+
492
+ return if controller_contains?(controller_path, callback)
493
+
494
+ current_user_callback =
495
+ /^\s*before_action :require_current_#{authentication_model_name}, only: %i\[ show edit update destroy \]\s*$/
496
+
497
+ inject_into_file(
498
+ controller_path,
499
+ after: current_user_callback
500
+ ) do
501
+ "\n #{callback}"
502
+ end
503
+ end
504
+
505
+ def add_authentication_resource_authorization(controller_path)
506
+ add_prevent_authentication_index_method(controller_path)
507
+ add_require_current_authentication_method(controller_path)
508
+ add_clear_authentication_session_method(controller_path)
509
+ end
510
+
511
+ def add_prevent_authentication_index_method(controller_path)
512
+ method_name = "def prevent_authentication_index"
513
+
514
+ return if controller_contains?(controller_path, method_name)
515
+
516
+ inject_private_method(
517
+ controller_path,
518
+ <<~RUBY
519
+ def prevent_authentication_index
520
+ redirect_to #{current_authentication_method}
521
+ end
522
+ RUBY
523
+ )
524
+ end
525
+
526
+ def add_require_current_authentication_method(controller_path)
527
+ method_name =
528
+ "def require_current_#{authentication_model_name}"
529
+
530
+ return if controller_contains?(controller_path, method_name)
531
+
532
+ inject_private_method(
533
+ controller_path,
534
+ <<~RUBY
535
+ def require_current_#{authentication_model_name}
536
+ return if @#{authentication_model_name} == #{current_authentication_method}
537
+
538
+ redirect_to root_path, alert: "Access denied."
539
+ end
540
+ RUBY
541
+ )
542
+ end
543
+
544
+ def add_clear_authentication_session_method(controller_path)
545
+ method_name = "def clear_authentication_session"
546
+
547
+ return if controller_contains?(controller_path, method_name)
548
+
549
+ inject_private_method(
550
+ controller_path,
551
+ <<~RUBY
552
+ def clear_authentication_session
553
+ session.delete(:#{authentication_model_name}_id)
554
+ end
555
+ RUBY
556
+ )
557
+ end
558
+
559
+ def inject_private_method(controller_path, method_body)
560
+ full_controller_path = File.join(
561
+ destination_root,
562
+ controller_path
563
+ )
564
+
565
+ content = File.read(full_controller_path)
566
+
567
+ class_end_position = content.rindex(/^end\s*$/)
568
+
569
+ unless class_end_position
570
+ raise Thor::Error,
571
+ "Could not find the closing class end in #{controller_path}"
572
+ end
573
+
574
+ indented_body = method_body.lines.map do |line|
575
+ line.strip.empty? ? "\n" : " #{line}"
576
+ end.join
577
+
578
+ updated_content = content.dup
579
+
580
+ updated_content.insert(
581
+ class_end_position,
582
+ "\n#{indented_body}"
583
+ )
584
+
585
+ File.write(
586
+ full_controller_path,
587
+ updated_content
588
+ )
589
+ end
590
+
591
+ def controller_contains?(controller_path, content)
592
+ full_controller_path = File.join(
593
+ destination_root,
594
+ controller_path
595
+ )
596
+
597
+ File.read(full_controller_path).include?(content)
598
+ end
599
+
600
+ def controller_matches?(controller_path, pattern)
601
+ full_controller_path = File.join(
602
+ destination_root,
603
+ controller_path
604
+ )
605
+
606
+ File.read(full_controller_path).match?(pattern)
607
+ end
608
+
254
609
  def protect_index_actions_for(controller_name)
255
610
  index_path = File.join(
256
611
  "app/views",
@@ -1,7 +1,9 @@
1
1
  /* -------------------------------------------------------
2
2
  Image lightbox
3
3
  ------------------------------------------------------- */
4
-
4
+ .lupe {
5
+ cursor: zoom-in;
6
+ }
5
7
  .resource-image__button,
6
8
  .resource-images__button {
7
9
  display: block;
@@ -38,7 +40,11 @@
38
40
  padding: 70px 90px 110px;
39
41
  box-sizing: border-box;
40
42
 
41
- background-color: rgba(0, 0, 0, 0.92);
43
+ background: var(--b4um-hero-teaser-background);
44
+ color: var(--b4um-hero-teaser-text);
45
+
46
+ backdrop-filter: blur(16px);
47
+ -webkit-backdrop-filter: blur(16px);
42
48
  }
43
49
 
44
50
  .image-lightbox[hidden] {
@@ -121,7 +127,7 @@ body.image-lightbox-open {
121
127
  top: 28px;
122
128
  left: 28px;
123
129
 
124
- color: #ffffff;
130
+ color: var(--b4um-hero-teaser-text);
125
131
 
126
132
  font-size: 0.9rem;
127
133
  font-weight: 500;
@@ -146,7 +152,7 @@ body.image-lightbox-open {
146
152
  border: 0;
147
153
 
148
154
  background: transparent;
149
- color: #ffffff;
155
+ color: var(--b4um-hero-teaser-text);
150
156
 
151
157
  font: inherit;
152
158
  font-size: 2.5rem;
@@ -165,11 +171,11 @@ body.image-lightbox-open {
165
171
  justify-content: center;
166
172
 
167
173
  width: 56px;
168
- height: 72px;
169
- padding: 0;
174
+ height: 56px;
175
+ padding-bottom: 12px;
170
176
 
171
177
  border: 0;
172
- border-radius: 8px;
178
+ border-radius: 50%;
173
179
 
174
180
  background-color: rgba(0, 0, 0, 0.35);
175
181
  color: #ffffff;
@@ -181,7 +187,7 @@ body.image-lightbox-open {
181
187
 
182
188
  cursor: pointer;
183
189
 
184
- transform: translateY(-50%);
190
+ transform: translateY(-100%);
185
191
 
186
192
  transition:
187
193
  background-color 0.18s ease,
@@ -189,7 +195,7 @@ body.image-lightbox-open {
189
195
  }
190
196
 
191
197
  .image-lightbox__navigation:hover {
192
- background-color: rgba(255, 255, 255, 0.16);
198
+ background-color: rgba(0, 0, 0, 0.16);
193
199
  }
194
200
 
195
201
  .image-lightbox__navigation--previous {
@@ -212,7 +218,7 @@ body.image-lightbox-open {
212
218
  bottom: 24px;
213
219
  left: 20px;
214
220
 
215
- color: #ffffff;
221
+ color: var(--b4um-hero-teaser-text);
216
222
 
217
223
  font-size: 0.85rem;
218
224
  text-align: center;
@@ -220,6 +226,82 @@ body.image-lightbox-open {
220
226
  overflow-wrap: anywhere;
221
227
  }
222
228
 
229
+ /* -------------------------------------------------------
230
+ Thumbnails
231
+ ------------------------------------------------------- */
232
+
233
+ .image-lightbox__thumbnails {
234
+ position: absolute;
235
+ right: 6rem;
236
+ bottom: 2rem;
237
+ left: 6rem;
238
+ z-index: 3;
239
+
240
+ display: flex;
241
+ align-items: center;
242
+ justify-content: center;
243
+
244
+ gap: 0.75rem;
245
+
246
+ overflow-x: auto;
247
+ overflow-y: hidden;
248
+
249
+ padding: 0.5rem;
250
+
251
+ scrollbar-width: none;
252
+ }
253
+
254
+ .image-lightbox__thumbnails::-webkit-scrollbar {
255
+ display: none;
256
+ }
257
+
258
+ .image-lightbox__thumbnail {
259
+ flex: 0 0 auto;
260
+
261
+ width: 72px;
262
+ height: 52px;
263
+
264
+ padding: 3px;
265
+
266
+ overflow: hidden;
267
+
268
+ border: 2px solid transparent;
269
+ border-radius: 10px;
270
+
271
+ background: rgba(255, 255, 255, 0.12);
272
+
273
+ opacity: 0.55;
274
+ cursor: pointer;
275
+
276
+ transition:
277
+ opacity 0.2s ease,
278
+ border-color 0.2s ease,
279
+ transform 0.2s ease;
280
+ }
281
+
282
+ .image-lightbox__thumbnail:hover {
283
+ opacity: 0.85;
284
+ }
285
+
286
+ .image-lightbox__thumbnail.is-active {
287
+ border-color: var(--b4um-hero-teaser-text);
288
+
289
+ opacity: 1;
290
+
291
+ transform: scale(1.06);
292
+ }
293
+
294
+ .image-lightbox__thumbnail img {
295
+ display: block;
296
+
297
+ width: 100%;
298
+ height: 100%;
299
+
300
+ border-radius: 6px;
301
+
302
+ object-fit: cover;
303
+ }
304
+
223
305
  @media (prefers-reduced-motion: reduce) {
224
306
  .image-lightbox__image--sliding {
225
307
  transition: none;
@@ -62,6 +62,10 @@
62
62
  --b4um-primary-shadow: rgba(46, 71, 210, 0.22);
63
63
  --b4um-primary-shadow-soft: rgba(46, 71, 210, 0.12);
64
64
  --b4um-danger-shadow: rgba(204, 0, 0, 0.18);
65
+
66
+ --b4um-hero-teaser-background: rgba(255, 255, 255, 0.55);
67
+ --b4um-hero-teaser-text: rgba(20, 20, 20, 0.9);
68
+ --b4um-hero-teaser-border: rgba(255, 255, 255, 0.3);
65
69
  }
66
70
 
67
71
  /* -------------------------------------------------------
@@ -128,5 +132,9 @@
128
132
  --b4um-primary-shadow-soft: rgba(142, 162, 255, 0.14);
129
133
  --b4um-danger-shadow: rgba(255, 133, 133, 0.18);
130
134
 
135
+ --b4um-hero-teaser-background: rgba(20, 20, 20, 0.55);
136
+ --b4um-hero-teaser-text: rgba(255, 255, 255, 0.85);
137
+ --b4um-hero-teaser-border: rgba(255, 255, 255, 0.18);
138
+
131
139
  color-scheme: dark;
132
140
  }
@@ -1,7 +1,17 @@
1
1
  import { Controller } from '@hotwired/stimulus'
2
2
 
3
3
  export default class extends Controller {
4
- static targets = ['item', 'overlay', 'stage', 'image', 'caption', 'counter', 'previousButton', 'nextButton']
4
+ static targets = [
5
+ 'item',
6
+ 'overlay',
7
+ 'stage',
8
+ 'image',
9
+ 'caption',
10
+ 'counter',
11
+ 'previousButton',
12
+ 'nextButton',
13
+ 'thumbnails'
14
+ ]
5
15
 
6
16
  connect() {
7
17
  this.currentIndex = 0
@@ -22,6 +32,7 @@ export default class extends Controller {
22
32
  if (index === -1) return
23
33
 
24
34
  this.currentIndex = index
35
+ this.buildThumbnails()
25
36
  this.showCurrentImage()
26
37
 
27
38
  this.overlayTarget.hidden = false
@@ -30,6 +41,64 @@ export default class extends Controller {
30
41
  this.overlayTarget.focus()
31
42
  }
32
43
 
44
+ buildThumbnails() {
45
+ if (!this.hasThumbnailsTarget) return
46
+
47
+ this.thumbnailsTarget.innerHTML = ''
48
+
49
+ this.itemTargets.forEach((item, index) => {
50
+ const button = document.createElement('button')
51
+ button.type = 'button'
52
+ button.className = 'image-lightbox__thumbnail'
53
+ button.setAttribute('aria-label', `Bild ${index + 1} anzeigen`)
54
+
55
+ const image = document.createElement('img')
56
+ image.src = item.dataset.imageLightboxUrl
57
+ image.alt = item.dataset.imageLightboxAlt || ''
58
+
59
+ button.appendChild(image)
60
+
61
+ button.addEventListener('click', (event) => {
62
+ event.stopPropagation()
63
+
64
+ if (index === this.currentIndex) return
65
+ if (this.isSliding) return
66
+
67
+ const direction = index > this.currentIndex ? 'next' : 'previous'
68
+
69
+ this.updateThumbnails(index)
70
+ this.slideTo(index, direction)
71
+ })
72
+
73
+ this.thumbnailsTarget.appendChild(button)
74
+ })
75
+
76
+ this.updateThumbnails()
77
+ }
78
+
79
+ updateThumbnails(activeIndex = this.currentIndex) {
80
+ if (!this.hasThumbnailsTarget) return
81
+
82
+ const thumbnails = Array.from(this.thumbnailsTarget.querySelectorAll('.image-lightbox__thumbnail'))
83
+
84
+ thumbnails.forEach((thumbnail, index) => {
85
+ const active = index === activeIndex
86
+
87
+ thumbnail.classList.toggle('is-active', active)
88
+ thumbnail.setAttribute('aria-current', active ? 'true' : 'false')
89
+ })
90
+
91
+ const activeThumbnail = thumbnails[activeIndex]
92
+
93
+ if (activeThumbnail) {
94
+ activeThumbnail.scrollIntoView({
95
+ behavior: this.prefersReducedMotion() ? 'auto' : 'smooth',
96
+ block: 'nearest',
97
+ inline: 'center'
98
+ })
99
+ }
100
+ }
101
+
33
102
  close() {
34
103
  if (this.isSliding) return
35
104
 
@@ -37,13 +106,6 @@ export default class extends Controller {
37
106
  document.body.classList.remove('image-lightbox-open')
38
107
  }
39
108
 
40
- /*
41
- closeOnBackground(event) {
42
- if (event.target !== this.overlayTarget) return
43
-
44
- this.close()
45
- }*/
46
-
47
109
  closeOnBackground(event) {
48
110
  if (this.isDragging || this.isSliding) return
49
111
 
@@ -207,6 +269,7 @@ export default class extends Controller {
207
269
 
208
270
  this.currentIndex = newIndex
209
271
  this.updateInformation()
272
+ this.updateThumbnails()
210
273
 
211
274
  this.dragPreviousImage = null
212
275
  this.dragNextImage = null
@@ -361,6 +424,7 @@ export default class extends Controller {
361
424
 
362
425
  this.currentIndex = index
363
426
  this.updateInformation()
427
+ this.updateThumbnails()
364
428
 
365
429
  this.isSliding = false
366
430
  }
@@ -1,20 +1,22 @@
1
1
  <div id="<%= plural_table_name %>" class="b4um-grid">
2
2
  <%% <%= plural_table_name %>.each_with_index do |<%= singular_table_name %>, index| %>
3
- <%% card_class =
4
- case index % 5
5
- when 0
6
- "b4um-card b4um-card--wide"
7
- when 1
8
- "b4um-card b4um-card--soft"
9
- when 3
10
- "b4um-card b4um-card--large"
11
- else
12
- "b4um-card"
13
- end
14
- %>
3
+ <%% card_class, preview_length =
4
+ case index % 5
5
+ when 0
6
+ ["b4um-card b4um-card--wide", 240]
7
+ when 1
8
+ ["b4um-card b4um-card--soft", 58]
9
+ when 3
10
+ ["b4um-card b4um-card--large", 1600]
11
+ else
12
+ ["b4um-card", 58]
13
+ end
14
+ %>
15
15
 
16
16
  <article class="<%%= card_class %>">
17
- <%%= render <%= singular_table_name %>, compact: true %>
17
+ <%%= render <%= singular_table_name %>,
18
+ compact: true,
19
+ preview_length: preview_length %>
18
20
 
19
21
  <div class="b4um-card__actions">
20
22
  <%%= link_to "Show",
@@ -76,11 +76,21 @@
76
76
  <%%= form.label :<%= attribute.name %>, class: "form-label" %>
77
77
  </div>
78
78
 
79
- <% elsif [:integer, :float, :decimal].include?(attribute.type) -%>
79
+ <% elsif attribute.type == :integer -%>
80
80
  <div class="form-field">
81
81
  <%%= form.number_field :<%= attribute.name %>,
82
82
  class: "form-input",
83
- placeholder: " " %>
83
+ placeholder: " ",
84
+ step: 1 %>
85
+ <%%= form.label :<%= attribute.name %>, class: "form-label" %>
86
+ </div>
87
+
88
+ <% elsif [:float, :decimal].include?(attribute.type) -%>
89
+ <div class="form-field">
90
+ <%%= form.number_field :<%= attribute.name %>,
91
+ class: "form-input",
92
+ placeholder: " ",
93
+ step: "any" %>
84
94
  <%%= form.label :<%= attribute.name %>, class: "form-label" %>
85
95
  </div>
86
96
 
@@ -49,6 +49,8 @@
49
49
  alt="">
50
50
  </div>
51
51
 
52
+ <div class="image-lightbox__thumbnails" data-image-lightbox-target="thumbnails"></div>
53
+
52
54
  <div class="image-lightbox__caption"
53
55
  data-image-lightbox-target="caption"></div>
54
56
  </div>
@@ -119,6 +121,8 @@
119
121
  &#8250;
120
122
  </button>
121
123
 
124
+ <div class="image-lightbox__thumbnails" data-image-lightbox-target="thumbnails"></div>
125
+
122
126
  <div class="image-lightbox__caption"
123
127
  data-image-lightbox-target="caption"></div>
124
128
  </div>
@@ -131,11 +135,23 @@
131
135
  <% elsif attribute.type == :text -%>
132
136
  <span class="resource-value">
133
137
  <%% if local_assigns[:compact] %>
134
- <%%= truncate(<%= singular_table_name %>.<%= attribute.name %>, length: 160) %>
138
+ <%%= truncate(
139
+ <%= singular_table_name %>.<%= attribute.name %>,
140
+ length: local_assigns.fetch(:preview_length, 160)
141
+ ) %>
135
142
  <%% else %>
136
143
  <%%= <%= singular_table_name %>.<%= attribute.name %> %>
137
144
  <%% end %>
138
145
  </span>
146
+ <% elsif attribute.type == :decimal -%>
147
+ <span class="resource-value">
148
+ <%%= number_with_precision(
149
+ <%= singular_table_name %>.<%= attribute.name %>,
150
+ precision: 2,
151
+ delimiter: ".",
152
+ separator: ","
153
+ ) %>
154
+ </span>
139
155
  <% else -%>
140
156
  <span class="resource-value"><%%= <%= singular_table_name %>.<%= attribute.name %> %></span>
141
157
  <% end -%>
@@ -20,6 +20,13 @@
20
20
  </span>
21
21
  <% elsif attribute.type == :text -%>
22
22
  <%%= truncate(<%= singular_table_name %>.<%= attribute.name %>, length: 100) %>
23
+ <% elsif attribute.type == :decimal -%>
24
+ <%%= number_with_precision(
25
+ <%= singular_table_name %>.<%= attribute.name %>,
26
+ precision: 2,
27
+ delimiter: ".",
28
+ separator: ","
29
+ ) %>
23
30
  <% else -%>
24
31
  <%%= <%= singular_table_name %>.<%= attribute.name %> %>
25
32
  <% end -%>
@@ -10,7 +10,8 @@ module B4umRichTextHelper
10
10
  first_element = document.element_children.first
11
11
  heading = b4um_preview_heading(first_element)
12
12
 
13
- plain_text = rich_text.to_plain_text.strip
13
+ document.css("action-text-attachment").remove
14
+ plain_text = document.text.strip
14
15
 
15
16
  if heading
16
17
  heading_text = heading.text.strip
@@ -43,6 +43,7 @@ configureHeadingAttributes()
43
43
  configureAlignmentAttributes()
44
44
  configureContainerAttributes()
45
45
  configureColorAttributes()
46
+ preserveNestedContainerHtmlAttributes()
46
47
  registerAlignmentElements()
47
48
 
48
49
  document.addEventListener('trix-initialize', (event) => {
@@ -77,6 +78,8 @@ function insertContainer(editor, tagName) {
77
78
  return
78
79
  }
79
80
 
81
+ const selectedText = editor.getDocument().toString().slice(selectedRange[0], selectedRange[1]).trim()
82
+
80
83
  const id = window.prompt('Bitte geben Sie die ID ein:')
81
84
 
82
85
  if (id === null) return
@@ -91,14 +94,23 @@ function insertContainer(editor, tagName) {
91
94
  editor.recordUndoEntry(`Insert ${tagName.toUpperCase()} Container`)
92
95
 
93
96
  editor.setSelectedRange(selectedRange)
97
+ editor.activateAttribute(tagName)
94
98
 
95
- const attributeName = tagName === 'div' ? 'b4umDiv' : tagName
99
+ const documentModel = editor.getDocument()
96
100
 
97
- editor.activateAttribute(attributeName)
101
+ const block = documentModel.getBlocks().find((block) => {
102
+ return block.getAttributes().includes(tagName) && block.toString().includes(selectedText)
103
+ })
98
104
 
99
- editor.composition.setHTMLAtributeAtPosition(selectedRange[0], 'id', trimmedId)
105
+ if (!block) {
106
+ console.error('b4um Trix: Container-Block wurde nicht gefunden.')
107
+ return
108
+ }
100
109
 
101
- editor.setSelectedRange(selectedRange)
110
+ const newBlock = block.addHTMLAttribute('id', trimmedId)
111
+ const newDocument = documentModel.replaceBlock(block, newBlock)
112
+
113
+ editor.loadDocument(newDocument)
102
114
  }
103
115
 
104
116
  /* -------------------------------------------------------------------------- */
@@ -106,8 +118,6 @@ function insertContainer(editor, tagName) {
106
118
  /* -------------------------------------------------------------------------- */
107
119
 
108
120
  function allowB4umHtmlAttributes() {
109
- const originalSetHTML = Trix.HTMLSanitizer.setHTML
110
-
111
121
  Trix.HTMLSanitizer.setHTML = function (element, html, options = {}) {
112
122
  const allowedAttributes = [
113
123
  'style',
@@ -121,10 +131,14 @@ function allowB4umHtmlAttributes() {
121
131
  ...(options.allowedAttributes || [])
122
132
  ]
123
133
 
124
- return originalSetHTML.call(this, element, html, {
134
+ const sanitizedElement = this.sanitize(html, {
125
135
  ...options,
126
136
  allowedAttributes: [...new Set(allowedAttributes)]
127
137
  })
138
+
139
+ const sanitizedHtml = sanitizedElement.getHTML ? sanitizedElement.getHTML() : sanitizedElement.outerHTML
140
+
141
+ element.innerHTML = sanitizedHtml
128
142
  }
129
143
  }
130
144
 
@@ -152,22 +166,17 @@ function configureAlignmentAttributes() {
152
166
  }
153
167
 
154
168
  function configureContainerAttributes() {
155
- Trix.config.blockAttributes.default.htmlAttributes = ['id']
156
-
157
- Trix.config.blockAttributes.b4umDiv = {
158
- tagName: 'div',
169
+ Trix.config.blockAttributes.section = {
170
+ tagName: 'section',
159
171
  parse: true,
160
172
  nestable: false,
161
173
  exclusive: true,
162
174
  group: false,
163
- htmlAttributes: ['id'],
164
- test(element) {
165
- return element.hasAttribute('id')
166
- }
175
+ htmlAttributes: ['id']
167
176
  }
168
177
 
169
- Trix.config.blockAttributes.section = {
170
- tagName: 'section',
178
+ Trix.config.blockAttributes.article = {
179
+ tagName: 'article',
171
180
  parse: true,
172
181
  nestable: false,
173
182
  exclusive: true,
@@ -175,8 +184,8 @@ function configureContainerAttributes() {
175
184
  htmlAttributes: ['id']
176
185
  }
177
186
 
178
- Trix.config.blockAttributes.article = {
179
- tagName: 'article',
187
+ Trix.config.blockAttributes.div = {
188
+ tagName: 'div',
180
189
  parse: true,
181
190
  nestable: false,
182
191
  exclusive: true,
@@ -185,6 +194,40 @@ function configureContainerAttributes() {
185
194
  }
186
195
  }
187
196
 
197
+ function preserveNestedContainerHtmlAttributes() {
198
+ const prototype = Trix.HTMLParser.prototype
199
+
200
+ if (prototype.b4umPreservesNestedContainerHtmlAttributes) return
201
+
202
+ const originalAppendBlockForAttributesWithElement = prototype.appendBlockForAttributesWithElement
203
+
204
+ prototype.appendBlockForAttributesWithElement = function (attributes, element, htmlAttributes = {}) {
205
+ let mergedHtmlAttributes = { ...htmlAttributes }
206
+ let parentElement = element.parentElement
207
+
208
+ while (parentElement && parentElement !== this.containerElement) {
209
+ const tagName = parentElement.tagName.toLowerCase()
210
+
211
+ const isB4umContainer = ['section', 'article', 'div'].includes(tagName)
212
+
213
+ if (isB4umContainer && parentElement.hasAttribute('id')) {
214
+ mergedHtmlAttributes = {
215
+ id: parentElement.getAttribute('id'),
216
+ ...mergedHtmlAttributes
217
+ }
218
+
219
+ break
220
+ }
221
+
222
+ parentElement = parentElement.parentElement
223
+ }
224
+
225
+ return originalAppendBlockForAttributesWithElement.call(this, attributes, element, mergedHtmlAttributes)
226
+ }
227
+
228
+ prototype.b4umPreservesNestedContainerHtmlAttributes = true
229
+ }
230
+
188
231
  function configureColorAttributes() {
189
232
  FOREGROUND_COLORS.forEach((color, index) => {
190
233
  Trix.config.textAttributes[`fgColor${index + 1}`] = {
@@ -312,9 +355,7 @@ function installContainerButtonHandlers(editorElement, toolbar) {
312
355
  if (!editor) return
313
356
 
314
357
  toolbar
315
- .querySelectorAll(
316
- '[data-trix-attribute="b4umDiv"], [data-trix-attribute="section"], [data-trix-attribute="article"]'
317
- )
358
+ .querySelectorAll('[data-trix-attribute="section"], [data-trix-attribute="article"], [data-trix-attribute="div"]')
318
359
  .forEach((button) => {
319
360
  if (button.dataset.b4umContainerHandler === 'true') return
320
361
 
@@ -334,9 +375,7 @@ function installContainerButtonHandlers(editorElement, toolbar) {
334
375
  event.preventDefault()
335
376
  event.stopImmediatePropagation()
336
377
 
337
- const tagName = attributeName === 'b4umDiv' ? 'div' : attributeName
338
-
339
- insertContainer(editor, tagName)
378
+ insertContainer(editor, attributeName)
340
379
  },
341
380
  true
342
381
  )
@@ -447,15 +486,6 @@ function containerButtonsTemplate() {
447
486
  <span
448
487
  class="trix-button-group trix-button-group--container-tools"
449
488
  data-trix-button-group="container-tools">
450
- <button
451
- type="button"
452
- class="trix-button"
453
- data-trix-attribute="b4umDiv"
454
- title="DIV"
455
- aria-label="DIV"
456
- tabindex="-1">
457
- div
458
- </button>
459
489
  <button
460
490
  type="button"
461
491
  class="trix-button"
@@ -474,6 +504,15 @@ function containerButtonsTemplate() {
474
504
  tabindex="-1">
475
505
  article
476
506
  </button>
507
+ <button
508
+ type="button"
509
+ class="trix-button"
510
+ data-trix-attribute="div"
511
+ title="DIV"
512
+ aria-label="DIV"
513
+ tabindex="-1">
514
+ div
515
+ </button>
477
516
  </span>
478
517
  `
479
518
  }
@@ -151,16 +151,49 @@ module B4um
151
151
 
152
152
  resource_name = name.underscore
153
153
 
154
+ preview_length =
155
+ 'local_assigns\.fetch\(:preview_length,\s*160\)'
156
+
157
+ resource_attribute =
158
+ "#{Regexp.escape(resource_name)}\\.#{Regexp.escape(attribute)}"
159
+
160
+ rich_text_pattern =
161
+ /truncate\(\s*#{resource_attribute},\s*length:\s*#{preview_length}\s*\)/m
162
+
163
+ plain_text_pattern =
164
+ /truncate\(\s*#{resource_attribute}\.to_plain_text,\s*length:\s*#{preview_length}\s*\)/m
165
+
166
+ replacement =
167
+ "b4um_rich_text_preview(" \
168
+ "#{resource_name}.#{attribute}, " \
169
+ "length: local_assigns.fetch(:preview_length, 160))"
170
+
171
+ legacy_replacement =
172
+ "b4um_rich_text_preview(" \
173
+ "#{resource_name}.#{attribute}, length: 160)"
174
+
175
+ gsub_file(
176
+ resource_partial_path,
177
+ rich_text_pattern,
178
+ replacement
179
+ )
180
+
181
+ gsub_file(
182
+ resource_partial_path,
183
+ plain_text_pattern,
184
+ replacement
185
+ )
186
+
154
187
  gsub_file(
155
188
  resource_partial_path,
156
189
  "truncate(#{resource_name}.#{attribute}, length: 160)",
157
- "b4um_rich_text_preview(#{resource_name}.#{attribute}, length: 160)"
190
+ legacy_replacement
158
191
  )
159
192
 
160
193
  gsub_file(
161
194
  resource_partial_path,
162
195
  "truncate(#{resource_name}.#{attribute}.to_plain_text, length: 160)",
163
- "b4um_rich_text_preview(#{resource_name}.#{attribute}, length: 160)"
196
+ legacy_replacement
164
197
  )
165
198
 
166
199
  partial = File.read(resource_partial_path)
@@ -243,7 +276,7 @@ module B4um
243
276
  unless partial.include?("blob.video?")
244
277
  gsub_file(
245
278
  action_text_blob_partial_path,
246
- /<% if blob\.representable\? %>\s*(<%= image_tag .+? %>)\s*<% end %>/m,
279
+ /<% if blob\.representable\? %>\s*<%= image_tag .+? %>\s*<% end %>/m,
247
280
  <<~ERB.chomp
248
281
  <% if blob.video? %>
249
282
  <%= video_tag rails_blob_path(blob, disposition: "inline"),
@@ -251,8 +284,8 @@ module B4um
251
284
  playsinline: true,
252
285
  preload: "metadata",
253
286
  class: "b4um-rich-text-video" %>
254
- <% elsif blob.representable? %>
255
- \\1
287
+ <% elsif blob.image? %>
288
+ <%= image_tag rails_blob_path(blob, disposition: "inline") %>
256
289
  <% end %>
257
290
  ERB
258
291
  )
@@ -266,13 +299,14 @@ module B4um
266
299
  action_text_blob_partial_path,
267
300
  /<%= image_tag (.+?) %>/,
268
301
  <<~ERB.chomp
269
- <%= image_tag \\1,
302
+ <%= image_tag rails_blob_path(blob, disposition: "inline"),
270
303
  data: {
271
304
  image_lightbox_target: "item",
272
305
  image_lightbox_url: rails_blob_path(blob, only_path: true),
273
306
  image_lightbox_alt: blob.filename.to_s,
274
307
  action: "click->image-lightbox#open"
275
- } %>
308
+ },
309
+ class: "lupe" %>
276
310
  ERB
277
311
  )
278
312
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: b4um_generators
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.7
4
+ version: 0.2.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Baum
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-09-28 00:00:00.000000000 Z
10
+ date: 2026-10-04 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: railties