effective_bootstrap 1.26.4 → 1.27.0

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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +16 -0
  3. data/app/assets/javascripts/effective_bootstrap/base.js.coffee +2 -0
  4. data/app/assets/javascripts/effective_bootstrap/form.js.coffee +1 -1
  5. data/app/assets/javascripts/effective_bootstrap/tabs.js +71 -21
  6. data/app/assets/stylesheets/effective_bootstrap/overrides.scss +5 -0
  7. data/app/controllers/concerns/effective_bootstrap/save_tabs.rb +2 -5
  8. data/app/helpers/effective_article_editor_helper.rb +0 -1
  9. data/app/helpers/effective_bootstrap_helper.rb +103 -86
  10. data/app/helpers/effective_editor_helper.rb +0 -2
  11. data/app/helpers/effective_form_builder_helper.rb +0 -1
  12. data/app/helpers/effective_icons_helper.rb +0 -2
  13. data/app/helpers/effective_iframe_helper.rb +1 -3
  14. data/app/helpers/effective_table_builder_helper.rb +0 -2
  15. data/app/models/effective/form_builder.rb +0 -2
  16. data/app/models/effective/form_input.rb +3 -4
  17. data/app/models/effective/form_inputs/article_editor.rb +6 -8
  18. data/app/models/effective/form_inputs/check_box.rb +0 -2
  19. data/app/models/effective/form_inputs/checks.rb +0 -4
  20. data/app/models/effective/form_inputs/ck_editor.rb +0 -2
  21. data/app/models/effective/form_inputs/clear.rb +0 -2
  22. data/app/models/effective/form_inputs/collection_input.rb +3 -4
  23. data/app/models/effective/form_inputs/date_field.rb +2 -4
  24. data/app/models/effective/form_inputs/datetime_field.rb +0 -2
  25. data/app/models/effective/form_inputs/delete.rb +0 -2
  26. data/app/models/effective/form_inputs/editor.rb +3 -5
  27. data/app/models/effective/form_inputs/email_cc_field.rb +0 -2
  28. data/app/models/effective/form_inputs/email_field.rb +0 -2
  29. data/app/models/effective/form_inputs/error_field.rb +0 -2
  30. data/app/models/effective/form_inputs/file_field.rb +13 -9
  31. data/app/models/effective/form_inputs/float_field.rb +0 -2
  32. data/app/models/effective/form_inputs/form_group.rb +0 -2
  33. data/app/models/effective/form_inputs/has_many.rb +6 -7
  34. data/app/models/effective/form_inputs/hidden_field.rb +0 -2
  35. data/app/models/effective/form_inputs/integer_field.rb +0 -1
  36. data/app/models/effective/form_inputs/number_text_field.rb +0 -2
  37. data/app/models/effective/form_inputs/password_field.rb +0 -2
  38. data/app/models/effective/form_inputs/percent_field.rb +0 -2
  39. data/app/models/effective/form_inputs/phone_field.rb +0 -2
  40. data/app/models/effective/form_inputs/price_field.rb +0 -2
  41. data/app/models/effective/form_inputs/radios.rb +2 -4
  42. data/app/models/effective/form_inputs/remote_link_to.rb +0 -2
  43. data/app/models/effective/form_inputs/reset.rb +0 -2
  44. data/app/models/effective/form_inputs/rich_text_area.rb +0 -2
  45. data/app/models/effective/form_inputs/save.rb +0 -2
  46. data/app/models/effective/form_inputs/search_field.rb +0 -2
  47. data/app/models/effective/form_inputs/select.rb +2 -3
  48. data/app/models/effective/form_inputs/select_or_text.rb +9 -10
  49. data/app/models/effective/form_inputs/static_field.rb +3 -3
  50. data/app/models/effective/form_inputs/submit.rb +1 -3
  51. data/app/models/effective/form_inputs/text_area.rb +0 -2
  52. data/app/models/effective/form_inputs/text_field.rb +0 -2
  53. data/app/models/effective/form_inputs/time_field.rb +0 -2
  54. data/app/models/effective/form_inputs/time_zone_select.rb +0 -2
  55. data/app/models/effective/form_inputs/url_field.rb +0 -2
  56. data/app/models/effective/form_logic.rb +1 -2
  57. data/app/models/effective/form_logics/hide_if.rb +0 -2
  58. data/app/models/effective/form_logics/show_if.rb +0 -2
  59. data/app/models/effective/form_logics/show_if_any.rb +0 -2
  60. data/app/models/effective/table_builder.rb +3 -2
  61. data/app/models/effective/table_row.rb +0 -1
  62. data/app/models/effective/table_rows/article_editor.rb +0 -2
  63. data/app/models/effective/table_rows/belongs_to.rb +2 -2
  64. data/app/models/effective/table_rows/boolean.rb +0 -2
  65. data/app/models/effective/table_rows/collection.rb +0 -2
  66. data/app/models/effective/table_rows/content_for.rb +0 -2
  67. data/app/models/effective/table_rows/date_field.rb +1 -3
  68. data/app/models/effective/table_rows/datetime_field.rb +1 -3
  69. data/app/models/effective/table_rows/effective_address.rb +0 -2
  70. data/app/models/effective/table_rows/email_field.rb +0 -2
  71. data/app/models/effective/table_rows/file_field.rb +0 -2
  72. data/app/models/effective/table_rows/has_many_card.rb +0 -1
  73. data/app/models/effective/table_rows/percent_field.rb +0 -1
  74. data/app/models/effective/table_rows/phone_field.rb +0 -2
  75. data/app/models/effective/table_rows/price_field.rb +0 -2
  76. data/app/models/effective/table_rows/text_area.rb +0 -2
  77. data/app/models/effective/table_rows/url_field.rb +0 -2
  78. data/lib/effective_bootstrap/engine.rb +0 -1
  79. data/lib/effective_bootstrap/version.rb +1 -1
  80. data/lib/effective_bootstrap.rb +0 -2
  81. data/lib/generators/effective_bootstrap/install_generator.rb +0 -1
  82. metadata +57 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ca3151713ddf3fe2ba10d003040a7337e4af6fc606d20e1228e6d8f397bc7ed8
4
- data.tar.gz: 8f51c987255a11ed33121e9c891a4615b2f05d449c32245e3a6fe3023645529f
3
+ metadata.gz: 4e155196cc973da70cd353e2f57f25f1e6cd1be03701eafcad73174562914588
4
+ data.tar.gz: 94387409afcd7c5158f616f326ff868cdf9cc2e47c79f30617de4cdf935fccfe
5
5
  SHA512:
6
- metadata.gz: 2b4a0afdb255cb6c3e2111aee0a2ace632aa7a5c36ad716d25b2ad0d31ece912033876f30d02e3e161bb0640011cb8914e4394923dbabf1c168a25e1e5f477bb
7
- data.tar.gz: 96385c23409c64e43152fd35e2ce4eb957b9e315a86029816f36f8c810a245901f9d4886e1f0ab9cd27200ebb205478a811c34b20e3479af6db51fbc6349677b
6
+ metadata.gz: b5c737e47f942dc665e5d4700af8ebf18fa7d6901211777b34ebdd9fe5af73229b3a5c3d6b7d3dd2476058f674ed91cd03005a68452ef34b74b59aa66d0af62e
7
+ data.tar.gz: 94f564bf1e08fe4e5a1f5b45985d003e25ba09f111531c6421994c009a0a55346aa2243792f039f5c887adcd9844374e2b281426d08a82bd8a3d9b999632d66b
data/README.md CHANGED
@@ -165,6 +165,10 @@ or
165
165
 
166
166
  https://getbootstrap.com/docs/4.0/components/navs/#tabs
167
167
 
168
+ Use `tab 'Logs', lazy: true do ... end` to defer an inactive tab's block until it is opened. The contents are fetched once from the same page and initialized with the existing Effective Bootstrap and DataTables hooks. Active tabs, non-GET renders, and inline datatable forms render normally. Repeated initialization is safe; failed loads wait for Retry. Use distinct `controls:` values when lazy tabs on the same page share a label, including with `unique: true`. Lazy tabs are intended for read-only content; unloaded form fields cannot be submitted. The follow-up GET renders only the requested tab and its ancestors; the controller, layout, and code outside tab blocks still run.
169
+
170
+ Use `?tab=logs` to select and render Logs in the initial server response. A `#tab-logs` fragment alone is only visible to the browser and therefore still requires a lazy request.
171
+
168
172
  ```haml
169
173
  = tabs do
170
174
  = tab 'Demographics' do
@@ -808,3 +812,15 @@ We are just extending these existing community projects for ease of use with Rai
808
812
  4. Push to the branch (`git push origin my-new-feature`)
809
813
  5. Bonus points for test coverage
810
814
  6. Create new Pull Request
815
+
816
+ ## Tests
817
+
818
+ From the gem repository, run:
819
+
820
+ ```sh
821
+ bundle install
822
+ bundle exec ruby test/tabs_test.rb
823
+ bundle exec ruby test/tabs_browser_test.rb
824
+ ```
825
+
826
+ The browser suite requires a local Chrome or Chromium installation.
@@ -1,6 +1,8 @@
1
1
  # This initializes all the effective_* form inputs also in this gem
2
2
  this.EffectiveBootstrap ||= new class
3
3
  initialize: (target) ->
4
+ initializeEffectiveLazyTabs(target)
5
+
4
6
  $(target || document).find('[data-input-js-options]:not(.initialized)').each (i, element) ->
5
7
  $element = $(element)
6
8
  options = $element.data('input-js-options')
@@ -8,7 +8,7 @@ this.EffectiveForm ||= new class
8
8
 
9
9
  validate: (form, event) ->
10
10
  $form = $(form)
11
- valid = form.checkValidity() && @allChecksValid($form)
11
+ valid = event?.submitter?.formNoValidate || (form.checkValidity() && @allChecksValid($form))
12
12
 
13
13
  @clearFlash()
14
14
  @reset($form) if $form.hasClass('was-validated')
@@ -3,33 +3,84 @@ $(document).on('show.bs.tab', function (e) {
3
3
  history.replaceState(history.state, null, '#' + e.target.getAttribute('id'));
4
4
  });
5
5
 
6
+ // Resolve one tab link; unique: true adds a numeric suffix to its controls.
7
+ function findEffectiveTab(href) {
8
+ if (!href) { return $(); }
9
+ var $links = $('.nav a[data-toggle="tab"]');
10
+ var $tab = $links.filter(function() { return this.getAttribute('href') === href; });
11
+ if (!$tab.length) {
12
+ $tab = $links.filter(function() {
13
+ var target = this.getAttribute('href') || '';
14
+ return target.startsWith(href + '-') && /^\d+$/.test(target.slice(href.length + 1));
15
+ });
16
+ }
17
+ return $tab.length === 1 ? $tab : $();
18
+ }
19
+
6
20
  // Display the tab based on form errors, the document hash, or ?tab= params
7
21
  function initializeEffectiveTabs() {
8
- var href = '';
9
-
10
- var $tab_with_error = $(".form-control.is-invalid").first().closest('.tab-pane')
11
-
12
- if ($tab_with_error.length > 0) {
13
- href = $tab_with_error.attr('id')
14
- } else if (document.location.hash.length > 0) {
15
- var hash = document.location.hash
16
- if(hash.startsWith('#tab-')) { href = hash.replace('#tab-', '') }
17
- } else if (document.location.search.length > 0) {
18
- var param = new URLSearchParams(document.location.search).get('tab') || ''
19
- if(param.length > 0) { href = param }
22
+ var hash = document.location.hash;
23
+ var tabId = $('.form-control.is-invalid').first().closest('.tab-pane').attr('id') || (hash.startsWith('#tab-') ? hash.slice(5) : new URLSearchParams(document.location.search).get('tab'));
24
+
25
+ if (tabId) {
26
+ var $tab = findEffectiveTab('#' + tabId);
27
+
28
+ $tab.parents('.tab-pane').each (function() { findEffectiveTab('#' + this.id).tab('show'); });
29
+ $tab.tab('show');
20
30
  }
21
31
 
22
- if (href.length > 0) {
23
- var $tab = $(".nav.nav-tabs").find("a[href^='#" + href + "']")
32
+ initializeEffectiveLazyTabs();
33
+ }
34
+
35
+ function initializeEffectiveLazyTabs(target) {
36
+ var selector = '.tab-pane[data-lazy-tab-url]:not(.initialized)';
37
+ $(target || document).find(selector).addBack(selector).filter(':visible').each(function() {
38
+ loadEffectiveTab(this);
39
+ });
40
+ }
24
41
 
25
- $tab.parents('.tab-pane').each (function() {
26
- $('.nav.nav-tabs').find("a[href^='#" + $(this).attr('id') + "']").tab('show')
27
- })
42
+ function loadEffectiveTab(pane) {
43
+ var $pane = $(pane);
44
+ if ($pane.hasClass('initialized') || $pane.data('lazy-tab-loading') || $pane.find('.effective-lazy-tab-retry').length) { return; }
28
45
 
29
- $tab.tab('show')
30
- }
46
+ var selector = '[data-lazy-tab="' + $.escapeSelector($pane.attr('data-lazy-tab')) + '"].initialized';
47
+ var tabKeys = $pane.parents('[data-lazy-tab]').addBack().map(function() { return this.getAttribute('data-lazy-tab'); }).get();
48
+ var sourceUrl = $pane.attr('data-lazy-tab-url');
49
+ var url = sourceUrl + (sourceUrl.includes('?') ? '&' : '?') + $.param({ _lazy_tab: tabKeys });
50
+
51
+ $pane.data('lazy-tab-loading', true).attr('aria-busy', 'true').html('<p>Loading...</p>');
52
+
53
+ // Loading into a detached container keeps late responses away from replacement panes.
54
+ var $content = $('<div>');
55
+
56
+ $content.load(url + ' ' + selector, function(_response, status) {
57
+ $pane.removeAttr('aria-busy').removeData('lazy-tab-loading');
58
+ if (!pane.isConnected) { return; }
59
+
60
+ var $loaded = $content.children();
61
+ if (status === 'error' || $loaded.length !== 1 || $loaded.attr('data-lazy-tab-url') !== $pane.attr('data-lazy-tab-url')) {
62
+ $pane.html('<p role="alert">Unable to load this tab. <button type="button" class="btn btn-link effective-lazy-tab-retry">Retry</button></p>');
63
+ return;
64
+ }
65
+
66
+ // Inserting content can itself trigger initialization.
67
+ $pane.addClass('initialized').html($loaded.html());
68
+ EffectiveBootstrap.initialize(pane);
69
+ $pane.trigger('effective-datatables:initialize');
70
+ });
31
71
  }
32
72
 
73
+ $(document).on('shown.bs.tab', 'a[data-toggle="tab"]', function(event) {
74
+ var pane = document.getElementById(event.currentTarget.getAttribute('aria-controls'));
75
+ if (pane) { initializeEffectiveLazyTabs(pane); }
76
+ });
77
+
78
+ $(document).on('click', '.effective-lazy-tab-retry', function(event) {
79
+ var $pane = $(event.currentTarget).closest('.tab-pane');
80
+ $pane.empty();
81
+ loadEffectiveTab($pane[0]);
82
+ });
83
+
33
84
  $(initializeEffectiveTabs);
34
85
  $(document).on('turbolinks:load turbo:load', initializeEffectiveTabs);
35
86
 
@@ -38,6 +89,5 @@ $(document).on('turbolinks:load turbo:load', initializeEffectiveTabs);
38
89
  $(document).on('click', '[data-click-tab]', function(event) {
39
90
  event.preventDefault();
40
91
 
41
- var href = $(event.currentTarget).attr('href');
42
- $('.nav.nav-tabs').find("a[href^='" + href + "']").tab('show');
92
+ findEffectiveTab($(event.currentTarget).attr('href')).tab('show');
43
93
  });
@@ -8,6 +8,11 @@
8
8
  // tabs do
9
9
  ul.nav.nav-tabs { margin-bottom: 1rem; }
10
10
 
11
+ @media print {
12
+ // Omit unloaded lazy panes even when print styles reveal inactive tabs.
13
+ .tab-content > .tab-pane[data-lazy-tab-url]:not(.initialized) { display: none !important; }
14
+ }
15
+
11
16
  // cursor on disabled radios
12
17
  .custom-control-input:disabled ~ .custom-control-label { cursor: no-drop; }
13
18
  .custom-control-input:disabled ~ .custom-control-label::after { cursor: no-drop; }
@@ -3,18 +3,15 @@ module EffectiveBootstrap
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
-
7
6
  # If there is a params[:_tabs], save it to the session
8
- before_action(if: -> { params[:_tabs] }) do
7
+ before_action(if: -> { params[:_tabs] && !(request.get? && params[:_lazy_tab].present?) }) do
9
8
  session[:_tabs] = params[:_tabs]
10
9
  end
11
10
 
12
11
  # After we're done, delete it from session
13
- after_action(if: -> { session[:_tabs] }) do
12
+ after_action(if: -> { session[:_tabs] && !(request.get? && params[:_lazy_tab].present?) }) do
14
13
  session.delete(:_tabs) unless response.redirect?
15
14
  end
16
-
17
15
  end
18
-
19
16
  end
20
17
  end
@@ -14,5 +14,4 @@ module EffectiveArticleEditorHelper
14
14
 
15
15
  rendered.html_safe
16
16
  end
17
-
18
17
  end
@@ -1,3 +1,5 @@
1
+ require 'benchmark'
2
+
1
3
  # frozen_string_literal: true
2
4
 
3
5
  # Boostrap4 Helpers
@@ -51,7 +53,7 @@ module EffectiveBootstrapHelper
51
53
  return if value.blank?
52
54
 
53
55
  badge_opts = opts
54
- badge_opts = {context: opts} unless badge_opts.kind_of?(Hash)
56
+ badge_opts = { context: opts } unless badge_opts.kind_of?(Hash)
55
57
 
56
58
  context = badge_opts[:context].to_s.sub('badge-', '').presence || 'secondary'
57
59
  badge_opts[:class] ||= "badge badge-#{context}"
@@ -110,11 +112,11 @@ module EffectiveBootstrapHelper
110
112
  label = (icon('clipboard', class: 'small-1') + ' ' + opts[:label]).html_safe
111
113
 
112
114
  content_tag(
113
- :button,
114
- label,
115
- class: ['btn-clipboard-copy', opts[:class]].compact.join(' '),
116
- type: 'button',
117
- 'data-clipboard': text,
115
+ :button,
116
+ label,
117
+ class: ['btn-clipboard-copy', opts[:class]].compact.join(' '),
118
+ type: 'button',
119
+ 'data-clipboard': text,
118
120
  'data-clipboard-label': label
119
121
  )
120
122
  end
@@ -175,13 +177,13 @@ module EffectiveBootstrapHelper
175
177
  link_class += ' collapsed' unless show
176
178
 
177
179
  # Figure out all the button / link options
178
- link_opts = {
179
- 'data-toggle': 'collapse',
180
- role: 'button',
181
- href: "##{id}",
182
- 'aria-controls': "##{id}",
183
- 'aria-expanded': show,
184
- class: link_class
180
+ link_opts = {
181
+ 'data-toggle': 'collapse',
182
+ role: 'button',
183
+ href: "##{id}",
184
+ 'aria-controls': "##{id}",
185
+ 'aria-expanded': show,
186
+ class: link_class
185
187
  }.merge(opts)
186
188
 
187
189
  # Normal collapse
@@ -209,14 +211,14 @@ module EffectiveBootstrapHelper
209
211
  # variations can be :dropup, :dropleft, :dropright
210
212
  # split can be true, false
211
213
  # right is to right align things
212
- DROPDOWN_SPLIT_OPTS = {class: "btn dropdown-toggle dropdown-toggle-split btn-sm btn-outline-primary", type: 'button', 'data-toggle': 'dropdown', 'aria-haspopup': true, 'aria-expanded': false}
213
- DROPDOWN_UNSPLIT_OPTS= {class: "btn dropdown-toggle btn-sm btn-outline-primary", type: 'button', 'data-toggle': 'dropdown', 'aria-haspopup': true, 'aria-expanded': false}
214
+ DROPDOWN_SPLIT_OPTS = { class: "btn dropdown-toggle dropdown-toggle-split btn-sm btn-outline-primary", type: 'button', 'data-toggle': 'dropdown', 'aria-haspopup': true, 'aria-expanded': false }
215
+ DROPDOWN_UNSPLIT_OPTS= { class: "btn dropdown-toggle btn-sm btn-outline-primary", type: 'button', 'data-toggle': 'dropdown', 'aria-haspopup': true, 'aria-expanded': false }
214
216
 
215
- DROPDOWN_DROPLEFT_GROUP_OPTS = {class: 'btn-group'}
216
- DROPDOWN_DROPLEFT_OPTS = {class: 'btn-group dropleft', role: 'group'}
217
+ DROPDOWN_DROPLEFT_GROUP_OPTS = { class: 'btn-group' }
218
+ DROPDOWN_DROPLEFT_OPTS = { class: 'btn-group dropleft', role: 'group' }
217
219
 
218
- DROPDOWN_MENU_OPTS = {class: 'dropdown-menu'}
219
- DROPDOWN_MENU_RIGHT_OPTS = {class: 'dropdown-menu dropdown-menu-right'}
220
+ DROPDOWN_MENU_OPTS = { class: 'dropdown-menu' }
221
+ DROPDOWN_MENU_RIGHT_OPTS = { class: 'dropdown-menu dropdown-menu-right' }
220
222
 
221
223
  DROPDOWN_BTN_CLASS = 'btn-sm btn-outline-primary'
222
224
  DROPDOWN_TOGGLE_DROPDOWN = "<span class='sr-only'>Toggle Dropdown</span>".html_safe
@@ -522,7 +524,7 @@ module EffectiveBootstrapHelper
522
524
  )
523
525
  last = (collection_count.to_f / per_page).ceil
524
526
 
525
- return unless (last > 1 || render_single_page) # If there's only 1 page, don't render a pagination at all.
527
+ return unless last > 1 || render_single_page # If there's only 1 page, don't render a pagination at all.
526
528
 
527
529
  # Build URL
528
530
  uri = URI(url || request.fullpath)
@@ -589,7 +591,7 @@ module EffectiveBootstrapHelper
589
591
  end
590
592
 
591
593
  # Let Kaminari override this method.
592
- alias_method(:paginate, :bootstrap_paginate) unless (respond_to?(:paginate) || defined?(Kaminari))
594
+ alias_method(:paginate, :bootstrap_paginate) unless respond_to?(:paginate) || defined?(Kaminari)
593
595
 
594
596
  def reveal_mail_to(email)
595
597
  raise('expected an email') unless email.kind_of?(String) && email.include?('@')
@@ -609,45 +611,56 @@ module EffectiveBootstrapHelper
609
611
  # If you pass active 'label' it will make that tab active. Otherwise first.
610
612
  # Unique will make sure the tab html IDs are unique
611
613
  # $('#tab-demographics').tab('show')
612
- def tabs(active: nil, unique: false, ignore_save_tab: false, benchmarks: EffectiveBootstrap.benchmarks, list: {}, content: {}, &block)
614
+ def tabs(active: nil, unique: false, ignore_save_tab: false, benchmarks: EffectiveBootstrap.benchmarks, list: {}, content: {}, vertical: false, &block)
615
+ previous_tabs = @_effective_tabs
613
616
  raise 'expected a block' unless block_given?
614
617
 
618
+ requested_tabs = previous_tabs ? previous_tabs[:requested] : (request.get? ? Array.wrap(params[:_lazy_tab]) : [])
619
+ state = { labels: [], requested: requested_tabs }
620
+ @_effective_tabs = state
621
+
615
622
  # The Active Tab might be set from a previous form submission, or passed into helper
616
- tab_active = session[:_tabs].try(:pop) unless ignore_save_tab
623
+ tab_active = session[:_tabs].try(:pop) unless ignore_save_tab || (request.get? && params[:_lazy_tab].present?)
617
624
  tab_active ||= active
618
625
 
619
- # If an active tab is passed, we validate we have a tab with that label, or fallback to first
620
- if tab_active.present?
621
- @_tab_mode = :validate
622
- @_tab_labels = []
626
+ # Validate selections against the available labels and controls before rendering.
627
+ if tab_active.present? || params[:tab].present?
628
+ state[:mode] = :validate
623
629
  yield
624
630
 
625
- tab_active = nil unless @_tab_labels.include?(tab_active)
631
+ tab_active = params[:tab] if state[:labels].include?(params[:tab])
632
+ tab_active = nil if tab_active && !state[:labels].include?(tab_active)
626
633
  end
627
634
 
628
- tab_active ||= :first if tab_active.nil?
629
- @_tab_unique = effective_bootstrap_unique_id if unique
630
- @_tab_benchmarks = {} if benchmarks # Enables benchmarks
635
+ tab_active = :first if tab_active.nil?
636
+ state[:unique] = effective_bootstrap_unique_id if unique
637
+ state[:benchmarks] = {} if benchmarks
638
+
639
+ list_options = if vertical
640
+ { class: 'nav flex-column nav-pills my-2', role: 'tablist', 'aria-orientation': :vertical }
641
+ else
642
+ { class: 'nav nav-tabs', role: 'tablist' }
643
+ end
631
644
 
632
645
  # Generate the html in two passes
633
- tabs_list = content_tag(:ul, {class: 'nav nav-tabs', role: 'tablist'}.merge(list)) do
634
- @_tab_mode = :tablist
635
- @_tab_active = tab_active
646
+ tabs_list = content_tag(vertical ? :div : :ul, list_options.merge(list)) do
647
+ state[:mode] = vertical ? :tablist_vertical : :tablist
648
+ state[:active] = tab_active
636
649
 
637
650
  yield # Yield to tab the first time
638
651
  end
639
652
 
640
- tabs_divs = content_tag(:div, {class: 'tab-content'}.merge(content)) do
641
- @_tab_mode = :content
642
- @_tab_active = tab_active
653
+ tabs_divs = content_tag(:div, { class: (vertical ? 'tab-content my-2' : 'tab-content') }.merge(content)) do
654
+ state[:mode] = :content
655
+ state[:active] = tab_active
643
656
 
644
657
  yield # Yield to tab the second time
645
658
  end
646
659
 
647
660
  if benchmarks
648
- total = @_tab_benchmarks.values.sum(&:real)
661
+ total = state[:benchmarks].values.sum(&:real)
649
662
 
650
- @_tab_benchmarks.each do |label, benchmark|
663
+ state[:benchmarks].each do |label, benchmark|
651
664
  percent = (benchmark.real / total * 100).round(0)
652
665
  amount = (benchmark.real * 1000).round(0)
653
666
 
@@ -665,75 +678,80 @@ module EffectiveBootstrapHelper
665
678
  tabs_list = tabs_list.html_safe
666
679
  end
667
680
 
668
- (tabs_list + tabs_divs)
669
- end
681
+ return tabs_list + tabs_divs unless vertical
670
682
 
671
- NUMBERS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
683
+ content_tag(:div, class: 'row border') do
684
+ content_tag(:div, tabs_list, class: 'col-3 border-right') + content_tag(:div, tabs_divs, class: 'col-9')
685
+ end
686
+ ensure
687
+ @_effective_tabs = previous_tabs
688
+ end
672
689
 
673
690
  def tab(resource, opts = {}, &block)
674
691
  return if resource.kind_of?(Class) && !EffectiveResources.authorized?(self, :index, resource)
692
+
693
+ state = @_effective_tabs || {}
675
694
  plural = resource.kind_of?(ActiveRecord::Base) == false
676
695
  label = opts[:label] || effective_bootstrap_human_name(resource, plural: opts.fetch(:plural, plural), prefer_model_name: true)
677
696
 
678
- (@_tab_labels.push(label) and return) if @_tab_mode == :validate
697
+ key = (opts[:controls] || label.to_s.parameterize.gsub('_', '-')).delete_prefix('#')
698
+ key = "item-#{key}" if key.match?(/\A\d/) # Can't start with a number
679
699
 
680
- controls = opts[:controls] || label.to_s.parameterize.gsub('_', '-')
681
- controls = "item-#{controls}" if NUMBERS.include?(controls[0]) # Can't start with a number
682
- controls = controls[1..-1] if controls[0] == '#'
683
- controls = "#{controls}-#{@_tab_unique}" if @_tab_unique
700
+ if state[:mode] == :validate
701
+ state[:labels].push(label, key)
702
+ return
703
+ end
684
704
 
685
- active = (@_tab_active == :first || @_tab_active == label)
705
+ controls = [key, state[:unique]].compact.join('-')
706
+ active = [:first, label, key].include?(state[:active])
686
707
 
687
- @_tab_active = nil if @_tab_active == :first
708
+ state[:active] = nil if state[:active] == :first
688
709
 
689
- if @_tab_mode == :tablist_vertical
690
- content_tag(:a, label, id: ('tab-' + controls), class: ['nav-link', ('active' if active)].compact.join(' '), href: '#' + controls, 'aria-controls': controls, 'aria-selected': active.to_s, 'data-toggle': 'tab', role: 'tab')
691
- elsif @_tab_mode == :tablist # Inserting the label into the tablist top
692
- content_tag(:li, class: 'nav-item') do
693
- content_tag(:a, label, id: ('tab-' + controls), class: ['nav-link', ('active' if active)].compact.join(' '), href: '#' + controls, 'aria-controls': controls, 'aria-selected': active.to_s, 'data-toggle': 'tab', role: 'tab')
694
- end
695
- else # Inserting the content into the tab itself
696
- classes = ['tab-pane', 'fade', ('show active' if active), opts[:class].presence].compact.join(' ')
697
- content_tag(:div, id: controls, class: classes, role: 'tabpanel', 'aria-labelledby': ('tab-' + controls), 'data-tab-label': label) do
698
- if @_tab_benchmarks.kind_of?(Hash)
699
- @_tab_benchmarks[label] = Benchmark.measure { yield }
700
- else
701
- yield
702
- end
703
- end
710
+ if [:tablist, :tablist_vertical].include?(state[:mode])
711
+ link = content_tag(:a, label, id: ('tab-' + controls), class: ['nav-link', ('active' if active)], href: '#' + controls, 'aria-controls': controls, 'aria-selected': active.to_s, 'data-toggle': 'tab', role: 'tab')
712
+ return state[:mode] == :tablist ? content_tag(:li, link, class: 'nav-item') : link
704
713
  end
705
- end
706
714
 
707
- def vertical_tabs(active: nil, unique: false, list: {}, content: {}, &block)
708
- raise 'expected a block' unless block_given?
715
+ requested_tabs = state.fetch(:requested) { request.get? ? Array.wrap(params[:_lazy_tab]) : [] }
716
+ return if requested_tabs.present? && !requested_tabs.include?(key)
709
717
 
710
- @_tab_mode = :tablist_vertical
711
- @_tab_active = (active || :first)
712
- @_tab_unique = effective_bootstrap_unique_id if unique
718
+ lazy = opts[:lazy] && request.get? && !(respond_to?(:inline_datatable?) && inline_datatable?)
719
+ deferred = lazy && !active && !requested_tabs.include?(key)
713
720
 
714
- content_tag(:div, class: 'row border') do
715
- content_tag(:div, class: 'col-3 border-right') do
716
- content_tag(:div, {class: 'nav flex-column nav-pills my-2', role: 'tablist', 'aria-orientation': :vertical}.merge(list)) do
717
- yield # Yield to tab the first time
718
- end
719
- end +
720
- content_tag(:div, class: 'col-9') do
721
- content_tag(:div, {class: 'tab-content my-2'}.merge(content)) do
722
- @_tab_mode = :content
723
- @_tab_active = (active || :first)
724
- yield # Yield to tab the second time
725
- end
721
+ source_url = if lazy
722
+ query = request.query_parameters.except('_tabs', '_lazy_tab').to_query.presence
723
+ [request.path, query].compact.join('?')
724
+ end
725
+
726
+ classes = ['tab-pane', 'fade', ('show active' if active), ('initialized' if lazy && !deferred), opts[:class]]
727
+ # Ancestor keys let fragment requests skip unrelated tab blocks.
728
+ content_tag(:div, id: controls, class: classes, role: 'tabpanel', 'aria-labelledby': ('tab-' + controls), 'data-tab-label': label,
729
+ 'data-lazy-tab': key, 'data-lazy-tab-url': source_url) do
730
+ # Once the target is reached, render its subtree normally.
731
+ state[:requested] = [] if requested_tabs.last == key
732
+ if deferred
733
+ content_tag(:p, 'Loading...')
734
+ elsif state[:benchmarks]
735
+ state[:benchmarks][label] = Benchmark.measure { yield }
736
+ else
737
+ yield
726
738
  end
739
+ ensure
740
+ state[:requested] = requested_tabs
727
741
  end
728
742
  end
729
743
 
744
+ def vertical_tabs(active: nil, unique: false, list: {}, content: {}, &block)
745
+ tabs(active: active, unique: unique, list: list, content: content, vertical: true, ignore_save_tab: true, benchmarks: false, &block)
746
+ end
747
+
730
748
  def merge_class_key(hash, value)
731
- return { :class => value } unless hash.kind_of?(Hash)
749
+ return { class: value } unless hash.kind_of?(Hash)
732
750
 
733
751
  if hash[:class].present?
734
- hash.merge!(:class => "#{hash[:class]} #{value}")
752
+ hash.merge!(class: "#{hash[:class]} #{value}")
735
753
  else
736
- hash.merge!(:class => value)
754
+ hash.merge!(class: value)
737
755
  end
738
756
  end
739
757
 
@@ -781,5 +799,4 @@ module EffectiveBootstrapHelper
781
799
  # Return the updated value
782
800
  @_effective_bootstrap_unique_id
783
801
  end
784
-
785
802
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EffectiveEditorHelper
4
-
5
4
  def effective_editor_tag(content, options = {})
6
5
  content = content.presence || '{}'
7
6
 
@@ -27,5 +26,4 @@ module EffectiveEditorHelper
27
26
 
28
27
  content_tag(:div, '', id: "ql-#{content.object_id}", class: 'effective_editor_content ql-effective', data: { 'input-js-options': input_js, 'content': content })
29
28
  end
30
-
31
29
  end
@@ -91,5 +91,4 @@ module EffectiveFormBuilderHelper
91
91
  ActionView::Base.field_error_proc = original
92
92
  end
93
93
  end
94
-
95
94
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EffectiveIconsHelper
4
-
5
4
  # icon('check', class: 'big-4')
6
5
  # icon('check', class: 'small-3')
7
6
  def icon(svg, options = {})
@@ -49,5 +48,4 @@ module EffectiveIconsHelper
49
48
  def remove_icon_to(path, options = {})
50
49
  icon_to('remove', path, { title: 'Remove' }.merge(options))
51
50
  end
52
-
53
51
  end
@@ -1,10 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EffectiveIframeHelper
4
-
5
4
  def iframe_srcdoc_tag(srcdoc)
6
5
  content_tag(
7
- :iframe,
6
+ :iframe,
8
7
  '',
9
8
  srcdoc: srcdoc,
10
9
  style: 'frameborder: 0; border: 0; width: 100%; height: 100%;',
@@ -12,5 +11,4 @@ module EffectiveIframeHelper
12
11
  scrolling: 'no'
13
12
  )
14
13
  end
15
-
16
14
  end
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EffectiveTableBuilderHelper
4
-
5
4
  def effective_table_with(resource, options = {}, &block)
6
5
  builder = Effective::TableBuilder.new(resource, self, options)
7
6
  builder.render(&block)
8
7
  end
9
-
10
8
  end
@@ -2,7 +2,6 @@
2
2
 
3
3
  module Effective
4
4
  class FormBuilder < ActionView::Helpers::FormBuilder
5
-
6
5
  attr_accessor :template, :layout, :action, :readonly, :disabled, :remote
7
6
 
8
7
  delegate :content_tag, to: :template
@@ -222,6 +221,5 @@ module Effective
222
221
 
223
222
  Effective::FormInputs::HasMany.new(name, options, builder: self).to_html(&block)
224
223
  end
225
-
226
224
  end
227
225
  end
@@ -12,7 +12,7 @@ module Effective
12
12
 
13
13
  DEFAULT_INPUT_GROUP_OPTIONS = { input_group: { class: 'input-group' }, prepend: false, append: false }
14
14
 
15
- HORIZONTAL_LABEL_OPTIONS = { class: 'col-sm-2 col-form-label'}
15
+ HORIZONTAL_LABEL_OPTIONS = { class: 'col-sm-2 col-form-label' }
16
16
  INLINE_LABEL_OPTIONS = { class: 'sr-only' }
17
17
 
18
18
  DEFAULT_FEEDBACK_OPTIONS = { valid: { class: 'valid-feedback' }, invalid: { class: 'invalid-feedback' } }
@@ -194,7 +194,7 @@ module Effective
194
194
  return nil if key.blank?
195
195
 
196
196
  hint = ::I18n.t(key)
197
- return nil if hint.include?(key) # missing translation
197
+ return nil if hint.include?(key) # missing translation
198
198
 
199
199
  hint
200
200
  end
@@ -259,7 +259,7 @@ module Effective
259
259
  end
260
260
 
261
261
  def required_options?(opts)
262
- return true unless (opts.key?(:if) || opts.key?(:unless))
262
+ return true unless opts.key?(:if) || opts.key?(:unless)
263
263
 
264
264
  # if opts[:if].respond_to?(:call)
265
265
  # return object.instance_exec(object, &opts[:if])
@@ -482,6 +482,5 @@ module Effective
482
482
  def input_js_options_method_name
483
483
  { method_name: "effective_#{self.class.name.split('::').last.underscore.chomp('_field')}" }
484
484
  end
485
-
486
485
  end
487
486
  end