diff --git a/spec/init_once_spec.rb b/spec/init_once_spec.rb new file mode 100644 index 0000000..2ffe52c --- /dev/null +++ b/spec/init_once_spec.rb @@ -0,0 +1,71 @@ +require 'spec_helper' + +describe 'initialising the same element twice', type: :feature, js: true do + + let(:today) { Time.now.utc.to_date } + + before { add_post_resource } + before { visit '/admin/posts' } + + def filter + page.find('.filter_date_range') + end + + def init_again(options = '') + page.execute_script( + "$('form.filter_form div.filter_date_range').date_range_ext_preset(#{options});" + ) + end + + # first, not find: with the bug there are two links, and an ambiguous-match + # error would mask whatever the example is actually asserting. + def pick_today + page.first('.filter_date_range a.btn_timerange').click + page.find('.block_timerange .btn_today').click + end + + def chosen_range + inputs = filter.all('input[type="text"]') + [inputs.first.value, inputs.last.value] + end + + # The auto-init was dead from jQuery 3 until it was restored, so the + # workaround -- calling the plugin by hand in active_admin.js -- is what + # host apps are carrying. Both run after an upgrade. + context 'with no options' do + before { init_again } + + it 'leaves one "Set range" link, not one per call' do + expect(filter).to have_css('a.btn_timerange', count: 1) + end + + it 'leaves one click handler, not one per call' do + count = page.evaluate_script( + "$._data($('form.filter_form div.filter_date_range')[0], 'events').click.length" + ) + + expect(count).to eq(1) + end + + it 'still fills the inputs' do + pick_today + + expect(chosen_range).to eq([today.strftime('%Y-%m-%d'), + (today + 1).strftime('%Y-%m-%d')]) + end + end + + # Sprockets puts the plugin's own ready callback ahead of anything the host + # appends to active_admin.js, so a guard that kept the first wiring would + # drop the host's configuration without a word. + context 'when the second call passes options' do + before { init_again('{ date_to_human_readable: true }') } + + it 'honours them instead of discarding them' do + pick_today + + expect(chosen_range).to eq([today.strftime('%Y-%m-%d'), + today.strftime('%Y-%m-%d')]) + end + end +end diff --git a/vendor/assets/javascripts/active_admin_date_range_preset.js b/vendor/assets/javascripts/active_admin_date_range_preset.js index 4ed2d9a..cdc33f5 100644 --- a/vendor/assets/javascripts/active_admin_date_range_preset.js +++ b/vendor/assets/javascripts/active_admin_date_range_preset.js @@ -54,6 +54,21 @@ $(function () { return this.each(function(i, el) { let $this = $(el); + // Re-initialising an element replaces its wiring instead of adding to + // it. The auto-init below was dead from jQuery 3 onwards, so hosts + // worked around it by calling the plugin themselves; now that it works + // again both run, and without this the label grows a second "Set range" + // whose handler tears down the first one's popup. + // + // Replacing rather than skipping matters: the auto-init registers its + // ready callback before anything a host adds to active_admin.js, so + // bailing out on the second call would silently drop the host's + // options. Reading the link itself rather than a data flag keeps the + // guard and the thing it guards from drifting apart across clone() and + // Turbo cache restores, which copy the node but not jQuery's data. + $this.find('a.btn_timerange').remove(); + $this.off('click.dateRangeExtPreset'); + // Per element, because data-show-time is read off the element itself. // Writing it back into the shared opts let one pair's setting reach // every other pair in the same call, and formatDate reads the flag at @@ -93,7 +108,7 @@ $(function () { lteq_input.val(formatDate(end, el_opts)); } - $this.on('click', '.btn_timerange', function(e) { + $this.on('click.dateRangeExtPreset', '.btn_timerange', function(e) { unbindClickEventBlockTimerange(); e.stopPropagation(); e.preventDefault();