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.
- checksums.yaml +4 -4
- data/README.md +16 -0
- data/app/assets/javascripts/effective_bootstrap/base.js.coffee +2 -0
- data/app/assets/javascripts/effective_bootstrap/form.js.coffee +1 -1
- data/app/assets/javascripts/effective_bootstrap/tabs.js +71 -21
- data/app/assets/stylesheets/effective_bootstrap/overrides.scss +5 -0
- data/app/controllers/concerns/effective_bootstrap/save_tabs.rb +2 -5
- data/app/helpers/effective_article_editor_helper.rb +0 -1
- data/app/helpers/effective_bootstrap_helper.rb +103 -86
- data/app/helpers/effective_editor_helper.rb +0 -2
- data/app/helpers/effective_form_builder_helper.rb +0 -1
- data/app/helpers/effective_icons_helper.rb +0 -2
- data/app/helpers/effective_iframe_helper.rb +1 -3
- data/app/helpers/effective_table_builder_helper.rb +0 -2
- data/app/models/effective/form_builder.rb +0 -2
- data/app/models/effective/form_input.rb +3 -4
- data/app/models/effective/form_inputs/article_editor.rb +6 -8
- data/app/models/effective/form_inputs/check_box.rb +0 -2
- data/app/models/effective/form_inputs/checks.rb +0 -4
- data/app/models/effective/form_inputs/ck_editor.rb +0 -2
- data/app/models/effective/form_inputs/clear.rb +0 -2
- data/app/models/effective/form_inputs/collection_input.rb +3 -4
- data/app/models/effective/form_inputs/date_field.rb +2 -4
- data/app/models/effective/form_inputs/datetime_field.rb +0 -2
- data/app/models/effective/form_inputs/delete.rb +0 -2
- data/app/models/effective/form_inputs/editor.rb +3 -5
- data/app/models/effective/form_inputs/email_cc_field.rb +0 -2
- data/app/models/effective/form_inputs/email_field.rb +0 -2
- data/app/models/effective/form_inputs/error_field.rb +0 -2
- data/app/models/effective/form_inputs/file_field.rb +13 -9
- data/app/models/effective/form_inputs/float_field.rb +0 -2
- data/app/models/effective/form_inputs/form_group.rb +0 -2
- data/app/models/effective/form_inputs/has_many.rb +6 -7
- data/app/models/effective/form_inputs/hidden_field.rb +0 -2
- data/app/models/effective/form_inputs/integer_field.rb +0 -1
- data/app/models/effective/form_inputs/number_text_field.rb +0 -2
- data/app/models/effective/form_inputs/password_field.rb +0 -2
- data/app/models/effective/form_inputs/percent_field.rb +0 -2
- data/app/models/effective/form_inputs/phone_field.rb +0 -2
- data/app/models/effective/form_inputs/price_field.rb +0 -2
- data/app/models/effective/form_inputs/radios.rb +2 -4
- data/app/models/effective/form_inputs/remote_link_to.rb +0 -2
- data/app/models/effective/form_inputs/reset.rb +0 -2
- data/app/models/effective/form_inputs/rich_text_area.rb +0 -2
- data/app/models/effective/form_inputs/save.rb +0 -2
- data/app/models/effective/form_inputs/search_field.rb +0 -2
- data/app/models/effective/form_inputs/select.rb +2 -3
- data/app/models/effective/form_inputs/select_or_text.rb +9 -10
- data/app/models/effective/form_inputs/static_field.rb +3 -3
- data/app/models/effective/form_inputs/submit.rb +1 -3
- data/app/models/effective/form_inputs/text_area.rb +0 -2
- data/app/models/effective/form_inputs/text_field.rb +0 -2
- data/app/models/effective/form_inputs/time_field.rb +0 -2
- data/app/models/effective/form_inputs/time_zone_select.rb +0 -2
- data/app/models/effective/form_inputs/url_field.rb +0 -2
- data/app/models/effective/form_logic.rb +1 -2
- data/app/models/effective/form_logics/hide_if.rb +0 -2
- data/app/models/effective/form_logics/show_if.rb +0 -2
- data/app/models/effective/form_logics/show_if_any.rb +0 -2
- data/app/models/effective/table_builder.rb +3 -2
- data/app/models/effective/table_row.rb +0 -1
- data/app/models/effective/table_rows/article_editor.rb +0 -2
- data/app/models/effective/table_rows/belongs_to.rb +2 -2
- data/app/models/effective/table_rows/boolean.rb +0 -2
- data/app/models/effective/table_rows/collection.rb +0 -2
- data/app/models/effective/table_rows/content_for.rb +0 -2
- data/app/models/effective/table_rows/date_field.rb +1 -3
- data/app/models/effective/table_rows/datetime_field.rb +1 -3
- data/app/models/effective/table_rows/effective_address.rb +0 -2
- data/app/models/effective/table_rows/email_field.rb +0 -2
- data/app/models/effective/table_rows/file_field.rb +0 -2
- data/app/models/effective/table_rows/has_many_card.rb +0 -1
- data/app/models/effective/table_rows/percent_field.rb +0 -1
- data/app/models/effective/table_rows/phone_field.rb +0 -2
- data/app/models/effective/table_rows/price_field.rb +0 -2
- data/app/models/effective/table_rows/text_area.rb +0 -2
- data/app/models/effective/table_rows/url_field.rb +0 -2
- data/lib/effective_bootstrap/engine.rb +0 -1
- data/lib/effective_bootstrap/version.rb +1 -1
- data/lib/effective_bootstrap.rb +0 -2
- data/lib/generators/effective_bootstrap/install_generator.rb +0 -1
- metadata +57 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4e155196cc973da70cd353e2f57f25f1e6cd1be03701eafcad73174562914588
|
|
4
|
+
data.tar.gz: 94387409afcd7c5158f616f326ff868cdf9cc2e47c79f30617de4cdf935fccfe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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
|
|
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
|
|
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
|
-
#
|
|
620
|
-
if tab_active.present?
|
|
621
|
-
|
|
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 =
|
|
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
|
|
629
|
-
|
|
630
|
-
|
|
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(:
|
|
634
|
-
|
|
635
|
-
|
|
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
|
-
|
|
642
|
-
|
|
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 =
|
|
661
|
+
total = state[:benchmarks].values.sum(&:real)
|
|
649
662
|
|
|
650
|
-
|
|
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
|
-
|
|
669
|
-
end
|
|
681
|
+
return tabs_list + tabs_divs unless vertical
|
|
670
682
|
|
|
671
|
-
|
|
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
|
-
(
|
|
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
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
700
|
+
if state[:mode] == :validate
|
|
701
|
+
state[:labels].push(label, key)
|
|
702
|
+
return
|
|
703
|
+
end
|
|
684
704
|
|
|
685
|
-
|
|
705
|
+
controls = [key, state[:unique]].compact.join('-')
|
|
706
|
+
active = [:first, label, key].include?(state[:active])
|
|
686
707
|
|
|
687
|
-
|
|
708
|
+
state[:active] = nil if state[:active] == :first
|
|
688
709
|
|
|
689
|
-
if
|
|
690
|
-
content_tag(:a, label, id: ('tab-' + controls), class: ['nav-link', ('active' if active)]
|
|
691
|
-
|
|
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
|
-
|
|
708
|
-
|
|
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
|
-
|
|
711
|
-
|
|
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
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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 { :
|
|
749
|
+
return { class: value } unless hash.kind_of?(Hash)
|
|
732
750
|
|
|
733
751
|
if hash[:class].present?
|
|
734
|
-
hash.merge!(:
|
|
752
|
+
hash.merge!(class: "#{hash[:class]} #{value}")
|
|
735
753
|
else
|
|
736
|
-
hash.merge!(:
|
|
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
|
|
@@ -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
|
|
@@ -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
|
|
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
|