b4um_generators 0.2.11 → 0.2.14

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -2
  3. data/README.de.md +127 -1
  4. data/README.md +126 -1
  5. data/lib/b4um_generators/version.rb +1 -1
  6. data/lib/generators/b4um/attachment/attachment_generator.rb +83 -23
  7. data/lib/generators/b4um/authentication/authentication_generator.rb +63 -0
  8. data/lib/generators/b4um/help/help_generator.rb +100 -3
  9. data/lib/generators/b4um/in_place/in_place_generator.rb +1231 -0
  10. data/lib/generators/b4um/in_place/templates/_flash.html.erb +15 -0
  11. data/lib/generators/b4um/in_place/templates/b4um_in_place_helper.rb +7 -0
  12. data/lib/generators/b4um/in_place/templates/display_attachment.html.erb.tt +154 -0
  13. data/lib/generators/b4um/in_place/templates/display_attachments.html.erb.tt +165 -0
  14. data/lib/generators/b4um/in_place/templates/display_boolean.html.erb.tt +18 -0
  15. data/lib/generators/b4um/in_place/templates/display_number.html.erb.tt +18 -0
  16. data/lib/generators/b4um/in_place/templates/display_radio.html.erb.tt +23 -0
  17. data/lib/generators/b4um/in_place/templates/display_rich_text.html.erb.tt +123 -0
  18. data/lib/generators/b4um/in_place/templates/display_select.html.erb.tt +23 -0
  19. data/lib/generators/b4um/in_place/templates/display_text.html.erb.tt +18 -0
  20. data/lib/generators/b4um/in_place/templates/edit_attachment.html.erb.tt +83 -0
  21. data/lib/generators/b4um/in_place/templates/edit_attachments.html.erb.tt +65 -0
  22. data/lib/generators/b4um/in_place/templates/edit_boolean.html.erb.tt +32 -0
  23. data/lib/generators/b4um/in_place/templates/edit_number.html.erb.tt +26 -0
  24. data/lib/generators/b4um/in_place/templates/edit_radio.html.erb.tt +32 -0
  25. data/lib/generators/b4um/in_place/templates/edit_rich_text.html.erb.tt +25 -0
  26. data/lib/generators/b4um/in_place/templates/edit_select.html.erb.tt +28 -0
  27. data/lib/generators/b4um/in_place/templates/edit_text.html.erb.tt +25 -0
  28. data/lib/generators/b4um/in_place/templates/in_place.css +935 -0
  29. data/lib/generators/b4um/in_place/templates/in_place_controller.js +115 -0
  30. data/lib/generators/b4um/install/install_generator.rb +18 -13
  31. data/lib/generators/b4um/install/templates/_flash.html.erb +15 -0
  32. data/lib/generators/b4um/install/templates/_footer.html.erb +6 -1
  33. data/lib/generators/b4um/install/templates/b4um/forms.css +127 -0
  34. data/lib/generators/b4um/install/templates/image_lightbox_controller.js +29 -0
  35. data/lib/generators/b4um/install/templates/rich_text_controller.js +22 -0
  36. data/lib/generators/b4um/install/templates/scroll_to_top_controller.js +12 -0
  37. data/lib/generators/b4um/scaffold/scaffold_generator.rb +106 -0
  38. data/lib/generators/b4um/scaffold/templates/_form.html.erb.tt +29 -0
  39. data/lib/generators/b4um/scaffold/templates/_resource.html.erb.tt +35 -0
  40. data/lib/generators/b4um/scaffold/templates/_table.html.erb.tt +31 -0
  41. data/lib/generators/b4um/trix/trix_generator.rb +104 -6
  42. metadata +25 -1
@@ -0,0 +1,115 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+
3
+ export default class extends Controller {
4
+ static values = {
5
+ url: String,
6
+ outsideSave: { type: Boolean, default: true },
7
+ cancelUrl: String
8
+ }
9
+
10
+ connect() {
11
+ this.handlePointerDown = this.handlePointerDown.bind(this)
12
+ this.handleKeyDown = this.handleKeyDown.bind(this)
13
+
14
+ if (this.element.querySelector('form')) {
15
+ this.rememberFormState()
16
+
17
+ document.addEventListener('pointerdown', this.handlePointerDown)
18
+ document.addEventListener('keydown', this.handleKeyDown)
19
+ this.focusEditor()
20
+ }
21
+ }
22
+
23
+ disconnect() {
24
+ document.removeEventListener('pointerdown', this.handlePointerDown)
25
+ document.removeEventListener('keydown', this.handleKeyDown)
26
+ }
27
+
28
+ edit(event) {
29
+ if (event?.type === 'keydown') {
30
+ event.preventDefault()
31
+ }
32
+
33
+ if (!this.hasUrlValue) return
34
+
35
+ const frame = this.element.closest('turbo-frame')
36
+ if (!frame) return
37
+
38
+ frame.src = this.urlValue
39
+ }
40
+
41
+ handlePointerDown(event) {
42
+ if (this.element.contains(event.target)) return
43
+
44
+ if (this.outsideSaveValue) {
45
+ this.save()
46
+ } else {
47
+ this.cancel()
48
+ }
49
+ }
50
+
51
+ handleKeyDown(event) {
52
+ if (event.key !== 'Escape') return
53
+
54
+ event.preventDefault()
55
+ this.cancel()
56
+ }
57
+
58
+ focusEditor() {
59
+ const control = this.element.querySelector(
60
+ 'input:not([type="hidden"]):not([type="file"]), select, textarea, trix-editor'
61
+ )
62
+
63
+ if (!control) return
64
+
65
+ requestAnimationFrame(() => {
66
+ control.focus()
67
+ })
68
+ }
69
+
70
+ rememberFormState() {
71
+ this.initialFormStates = new WeakMap()
72
+
73
+ this.element.querySelectorAll('form').forEach((form) => {
74
+ this.initialFormStates.set(form, new URLSearchParams(new FormData(form)).toString())
75
+ })
76
+ }
77
+
78
+ formChanged(form) {
79
+ if (!this.initialFormStates?.has(form)) return true
80
+
81
+ const initialState = this.initialFormStates.get(form)
82
+ const currentState = new URLSearchParams(new FormData(form)).toString()
83
+
84
+ return initialState !== currentState
85
+ }
86
+
87
+ save(event) {
88
+ const form = event?.target?.closest('form') || this.element.querySelector('form')
89
+
90
+ if (!form) return
91
+
92
+ const fileInput = event?.target instanceof HTMLInputElement && event.target.type === 'file' ? event.target : null
93
+
94
+ if (fileInput?.files?.length) {
95
+ form.requestSubmit()
96
+ return
97
+ }
98
+
99
+ if (!this.formChanged(form)) {
100
+ this.cancel()
101
+ return
102
+ }
103
+
104
+ form.requestSubmit()
105
+ }
106
+
107
+ cancel() {
108
+ if (!this.hasCancelUrlValue) return
109
+
110
+ const frame = this.element.closest('turbo-frame')
111
+ if (!frame) return
112
+
113
+ frame.src = this.cancelUrlValue
114
+ }
115
+ }
@@ -333,6 +333,13 @@ module B4um
333
333
  )
334
334
  end
335
335
 
336
+ def copy_flash
337
+ copy_file(
338
+ "_flash.html.erb",
339
+ "app/views/shared/_flash.html.erb"
340
+ )
341
+ end
342
+
336
343
  def copy_navigation_controller
337
344
  copy_file "navigation_controller.js",
338
345
  "app/javascript/controllers/navigation_controller.js"
@@ -385,6 +392,16 @@ module B4um
385
392
  "app/javascript/controllers/image_lightbox_controller.js"
386
393
  end
387
394
 
395
+ def copy_scroll_to_top_controller
396
+ copy_file "scroll_to_top_controller.js",
397
+ "app/javascript/controllers/scroll_to_top_controller.js"
398
+ end
399
+
400
+ def copy_rich_text_controller
401
+ copy_file "rich_text_controller.js",
402
+ "app/javascript/controllers/rich_text_controller.js"
403
+ end
404
+
388
405
  def copy_dismissible_controller
389
406
  copy_file "dismissible_controller.js",
390
407
  "app/javascript/controllers/dismissible_controller.js"
@@ -470,19 +487,7 @@ module B4um
470
487
  <%= render "shared/navigation" %>
471
488
 
472
489
  <main class="container">
473
- <% flash.each do |type, message| %>
474
- <div class="flash <%= "flash--" + type.to_s %>"
475
- data-controller="dismissible">
476
- <%= message %>
477
-
478
- <button type="button"
479
- class="flash__close"
480
- data-action="dismissible#dismiss"
481
- aria-label="Close">
482
- &times;
483
- </button>
484
- </div>
485
- <% end %>
490
+ <%= render "shared/flash" %>
486
491
  ERB
487
492
  )
488
493
 
@@ -0,0 +1,15 @@
1
+ <div id="flash-messages">
2
+ <% flash.each do |type, message| %>
3
+ <div class="flash <%= "flash--" + type.to_s %>"
4
+ data-controller="dismissible">
5
+ <%= message %>
6
+
7
+ <button type="button"
8
+ class="flash__close"
9
+ data-action="dismissible#dismiss"
10
+ aria-label="Close">
11
+ &times;
12
+ </button>
13
+ </div>
14
+ <% end %>
15
+ </div>
@@ -1,5 +1,10 @@
1
1
  <footer class="b4um-footer">
2
- <a class="b4um-footer__back-to-top" href="#b4um-page-top">
2
+ <a
3
+ class="b4um-footer__back-to-top"
4
+ href="#b4um-page-top"
5
+ data-controller="scroll-to-top"
6
+ data-action="click->scroll-to-top#scroll"
7
+ >
3
8
  Zurück zum Seitenanfang
4
9
  </a>
5
10
 
@@ -275,6 +275,133 @@
275
275
  display: block;
276
276
  }
277
277
 
278
+ /* -------------------------------------------------------
279
+ Selects
280
+ ------------------------------------------------------- */
281
+
282
+ .form-field--select .form-label {
283
+ top: 0;
284
+
285
+ color: var(--b4um-primary);
286
+
287
+ transform: translateY(-50%) scale(0.82);
288
+ }
289
+
290
+ .form-select {
291
+ width: 100%;
292
+ margin: 0;
293
+ padding: 1rem 3rem 1rem 1.15rem;
294
+
295
+ box-sizing: border-box;
296
+
297
+ border: 1px solid var(--b4um-border);
298
+ border-radius: 8px;
299
+
300
+ background-color: var(--b4um-surface);
301
+ color: var(--b4um-text);
302
+
303
+ font: inherit;
304
+
305
+ cursor: pointer;
306
+
307
+ transition:
308
+ border-color 0.18s ease,
309
+ box-shadow 0.18s ease,
310
+ background-color 0.18s ease;
311
+ }
312
+
313
+ .form-select:hover {
314
+ border-color: var(--b4um-border-hover);
315
+ }
316
+
317
+ .form-select:focus {
318
+ outline: none;
319
+
320
+ border-color: var(--b4um-primary);
321
+ box-shadow: 0 0 0 3px var(--b4um-primary-soft);
322
+ }
323
+
324
+ /* -------------------------------------------------------
325
+ Radio buttons
326
+ ------------------------------------------------------- */
327
+
328
+ .form-field--radio {
329
+ padding: 0.75rem 0;
330
+ }
331
+
332
+ .form-field--radio > .form-label {
333
+ position: static;
334
+
335
+ display: block;
336
+
337
+ margin-bottom: 0.75rem;
338
+ padding: 0;
339
+
340
+ background: none;
341
+ color: var(--b4um-text-secondary);
342
+
343
+ transform: none;
344
+ }
345
+
346
+ .form-radio-options {
347
+ display: flex;
348
+ flex-wrap: wrap;
349
+ align-items: center;
350
+ gap: 0.75rem 1.25rem;
351
+ }
352
+
353
+ .form-radio-option {
354
+ display: inline-flex;
355
+ align-items: center;
356
+ gap: 0.5rem;
357
+
358
+ color: var(--b4um-text);
359
+
360
+ cursor: pointer;
361
+ }
362
+
363
+ .form-radio {
364
+ appearance: none;
365
+ -webkit-appearance: none;
366
+
367
+ width: 18px;
368
+ height: 18px;
369
+ margin: 0;
370
+
371
+ flex: 0 0 18px;
372
+
373
+ border: 1px solid var(--b4um-border-checkbox);
374
+ border-radius: 50%;
375
+
376
+ background-color: var(--b4um-surface);
377
+
378
+ cursor: pointer;
379
+
380
+ transition:
381
+ border-color 0.18s ease,
382
+ box-shadow 0.18s ease,
383
+ transform 0.12s ease;
384
+ }
385
+
386
+ .form-radio:hover {
387
+ border-color: var(--b4um-primary);
388
+ }
389
+
390
+ .form-radio:checked {
391
+ border: 5px solid var(--b4um-primary);
392
+ }
393
+
394
+ .form-radio:focus-visible {
395
+ outline: none;
396
+
397
+ border-color: var(--b4um-primary);
398
+ box-shadow: 0 0 0 3px var(--b4um-primary-soft);
399
+ }
400
+
401
+ .form-radio:active {
402
+ transform: scale(0.94);
403
+ }
404
+
278
405
  /* -------------------------------------------------------
279
406
  Checkboxes
280
407
  ------------------------------------------------------- */
@@ -21,6 +21,35 @@ export default class extends Controller {
21
21
  this.dragStartX = 0
22
22
  this.dragCurrentX = 0
23
23
  this.dragPointerId = null
24
+
25
+ this.prepareActionTextImages()
26
+ }
27
+
28
+ prepareActionTextImages() {
29
+ const images = this.element.querySelectorAll('.trix-content figure.attachment img')
30
+
31
+ images.forEach((image) => {
32
+ if (image.closest('[data-image-lightbox-target="item"]')) return
33
+
34
+ const item = image.closest('a') || image
35
+
36
+ item.dataset.imageLightboxTarget = 'item'
37
+ item.dataset.imageLightboxUrl = image.currentSrc || image.src
38
+ item.dataset.imageLightboxAlt = image.alt || ''
39
+
40
+ const actions = [item.dataset.action, 'click->image-lightbox#open']
41
+
42
+ if (item.tagName !== 'A') {
43
+ actions.push('keydown.enter->image-lightbox#open', 'keydown.space->image-lightbox#open')
44
+
45
+ item.setAttribute('role', 'button')
46
+ item.setAttribute('tabindex', '0')
47
+ }
48
+
49
+ item.dataset.action = actions.filter(Boolean).join(' ')
50
+
51
+ item.classList.add('in-place-rich-text__lightbox-image')
52
+ })
24
53
  }
25
54
 
26
55
  open(event) {
@@ -0,0 +1,22 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+
3
+ export default class extends Controller {
4
+ connect() {
5
+ this.prepareLinks()
6
+ }
7
+
8
+ prepareLinks() {
9
+ const links = this.element.querySelectorAll('a[href]')
10
+
11
+ links.forEach((link) => {
12
+ link.setAttribute('target', '_blank')
13
+
14
+ const rel = new Set((link.getAttribute('rel') || '').split(/\s+/).filter(Boolean))
15
+
16
+ rel.add('noopener')
17
+ rel.add('noreferrer')
18
+
19
+ link.setAttribute('rel', Array.from(rel).join(' '))
20
+ })
21
+ }
22
+ }
@@ -0,0 +1,12 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+
3
+ export default class extends Controller {
4
+ scroll(event) {
5
+ event.preventDefault()
6
+
7
+ window.scrollTo({
8
+ top: 0,
9
+ behavior: 'smooth'
10
+ })
11
+ }
12
+ }
@@ -25,6 +25,27 @@ module B4um
25
25
  enum: %w[bento table],
26
26
  desc: "Index layout: bento or table"
27
27
 
28
+ class_option :select,
29
+ type: :string,
30
+ desc: "Renders a field as a select list. Example: status:Active=Aktiv,Inactive=Inaktiv"
31
+
32
+ class_option :radio,
33
+ type: :string,
34
+ desc: "Renders a field as radio buttons. Example: condition:new=Neu,used=Gebraucht"
35
+
36
+ def validate_editor_options
37
+ configurations = {
38
+ select: select_configuration,
39
+ radio: radio_configuration
40
+ }
41
+
42
+ configurations.each do |option_name, configuration|
43
+ validate_editor_configuration(option_name, configuration)
44
+ end
45
+
46
+ validate_editor_configuration_conflict(configurations)
47
+ end
48
+
28
49
  def create_b4um_form
29
50
  form_path = File.join(
30
51
  "app/views",
@@ -328,6 +349,91 @@ module B4um
328
349
  navigation_link.chomp
329
350
  )
330
351
  end
352
+
353
+ private
354
+
355
+ def validate_editor_configuration(option_name, configuration)
356
+ return unless options[option_name].present?
357
+
358
+ unless configuration
359
+ raise Thor::Error,
360
+ "Invalid --#{option_name} configuration. " \
361
+ "Expected FIELD:VALUE[,VALUE...]"
362
+ end
363
+
364
+ field_names = attributes.map(&:name)
365
+
366
+ return if field_names.include?(configuration[:field])
367
+
368
+ raise Thor::Error,
369
+ "--#{option_name} field #{configuration[:field]} " \
370
+ "must be one of the generated fields: #{field_names.join(", ")}"
371
+ end
372
+
373
+ def validate_editor_configuration_conflict(configurations)
374
+ select = configurations[:select]
375
+ radio = configurations[:radio]
376
+
377
+ return unless select && radio
378
+ return unless select[:field] == radio[:field]
379
+
380
+ raise Thor::Error,
381
+ "Field #{select[:field]} cannot use both --select and --radio"
382
+ end
383
+
384
+ def select_configuration
385
+ editor_configuration(:select)
386
+ end
387
+
388
+ def radio_configuration
389
+ editor_configuration(:radio)
390
+ end
391
+
392
+ def editor_configuration(option_name)
393
+ value = options[option_name]
394
+ return unless value.present?
395
+
396
+ field, values = value.split(":", 2)
397
+
398
+ return unless field.present? && values.present?
399
+
400
+ choices = editor_choices(values)
401
+ return if choices.empty?
402
+
403
+ {
404
+ field: field,
405
+ values: choices
406
+ }
407
+ end
408
+
409
+ def editor_choices(values)
410
+ values.split(",").filter_map do |choice|
411
+ value, label = choice.strip.split("=", 2)
412
+
413
+ next if value.blank?
414
+
415
+ {
416
+ value: value,
417
+ label: label.presence || value
418
+ }
419
+ end
420
+ end
421
+
422
+ def select_field?(field)
423
+ configuration = select_configuration
424
+
425
+ configuration &&
426
+ configuration[:field] == field &&
427
+ configuration[:values].any?
428
+ end
429
+
430
+ def radio_field?(field)
431
+ configuration = radio_configuration
432
+
433
+ configuration &&
434
+ configuration[:field] == field &&
435
+ configuration[:values].any?
436
+ end
331
437
  end
332
438
  end
333
439
  end
@@ -62,6 +62,35 @@
62
62
  <%%= form.label :<%= attribute.name %>, class: "form-label" %>
63
63
  </div>
64
64
 
65
+ <% elsif select_field?(attribute.name) -%>
66
+ <div class="form-field form-field--select">
67
+ <%%= form.label :<%= attribute.name %>, class: "form-label" %>
68
+
69
+ <%%= form.select :<%= attribute.name %>,
70
+ <%= select_configuration[:values].inspect %>.map { |choice|
71
+ [choice[:label], choice[:value]]
72
+ },
73
+ {},
74
+ class: "form-select" %>
75
+ </div>
76
+
77
+ <% elsif radio_field?(attribute.name) -%>
78
+ <div class="form-field form-field--radio">
79
+ <%%= form.label :<%= attribute.name %>, class: "form-label" %>
80
+
81
+ <div class="form-radio-options">
82
+ <%% <%= radio_configuration[:values].inspect %>.each do |choice| %>
83
+ <label class="form-radio-option">
84
+ <%%= form.radio_button :<%= attribute.name %>,
85
+ choice[:value],
86
+ class: "form-radio" %>
87
+
88
+ <span><%%= choice[:label] %></span>
89
+ </label>
90
+ <%% end %>
91
+ </div>
92
+ </div>
93
+
65
94
  <% elsif attribute.type == :boolean -%>
66
95
  <div class="form-field form-field--checkbox">
67
96
  <%%= form.check_box :<%= attribute.name %>, class: "form-checkbox" %>
@@ -130,6 +130,41 @@
130
130
  <span class="resource-value resource-value--empty">No images</span>
131
131
  <%% end %>
132
132
  </div>
133
+ <% elsif select_field?(attribute.name) -%>
134
+ <% if attribute.name == "status" -%>
135
+ <span class="b4um-badge">
136
+ <%%=
137
+ <%= select_configuration[:values].inspect %>.to_h {
138
+ |choice| [choice[:value], choice[:label]]
139
+ }.fetch(
140
+ <%= singular_table_name %>.<%= attribute.name %>,
141
+ <%= singular_table_name %>.<%= attribute.name %>
142
+ )
143
+ %>
144
+ </span>
145
+ <% else -%>
146
+ <span class="resource-value">
147
+ <%%=
148
+ <%= select_configuration[:values].inspect %>.to_h {
149
+ |choice| [choice[:value], choice[:label]]
150
+ }.fetch(
151
+ <%= singular_table_name %>.<%= attribute.name %>,
152
+ <%= singular_table_name %>.<%= attribute.name %>
153
+ )
154
+ %>
155
+ </span>
156
+ <% end -%>
157
+ <% elsif radio_field?(attribute.name) -%>
158
+ <span class="resource-value">
159
+ <%%=
160
+ <%= radio_configuration[:values].inspect %>.to_h {
161
+ |choice| [choice[:value], choice[:label]]
162
+ }.fetch(
163
+ <%= singular_table_name %>.<%= attribute.name %>,
164
+ <%= singular_table_name %>.<%= attribute.name %>
165
+ )
166
+ %>
167
+ </span>
133
168
  <% elsif attribute.name == "status" -%>
134
169
  <span class="b4um-badge"><%%= <%= singular_table_name %>.<%= attribute.name %> %></span>
135
170
  <% elsif attribute.type == :text -%>
@@ -14,7 +14,38 @@
14
14
  <tr>
15
15
  <% attributes.each do |attribute| -%>
16
16
  <td>
17
+ <% if select_field?(attribute.name) -%>
17
18
  <% if attribute.name == "status" -%>
19
+ <span class="b4um-badge">
20
+ <%%=
21
+ <%= select_configuration[:values].inspect %>.to_h {
22
+ |choice| [choice[:value], choice[:label]]
23
+ }.fetch(
24
+ <%= singular_table_name %>.<%= attribute.name %>,
25
+ <%= singular_table_name %>.<%= attribute.name %>
26
+ )
27
+ %>
28
+ </span>
29
+ <% else -%>
30
+ <%%=
31
+ <%= select_configuration[:values].inspect %>.to_h {
32
+ |choice| [choice[:value], choice[:label]]
33
+ }.fetch(
34
+ <%= singular_table_name %>.<%= attribute.name %>,
35
+ <%= singular_table_name %>.<%= attribute.name %>
36
+ )
37
+ %>
38
+ <% end -%>
39
+ <% elsif radio_field?(attribute.name) -%>
40
+ <%%=
41
+ <%= radio_configuration[:values].inspect %>.to_h {
42
+ |choice| [choice[:value], choice[:label]]
43
+ }.fetch(
44
+ <%= singular_table_name %>.<%= attribute.name %>,
45
+ <%= singular_table_name %>.<%= attribute.name %>
46
+ )
47
+ %>
48
+ <% elsif attribute.name == "status" -%>
18
49
  <span class="b4um-badge">
19
50
  <%%= <%= singular_table_name %>.<%= attribute.name %> %>
20
51
  </span>