From a03b4b476fa9b09f503ea2037c98bf18aa92092e Mon Sep 17 00:00:00 2001 From: Steven Bull Date: Sat, 28 Apr 2018 17:59:29 -0700 Subject: [PATCH] Add config.horizontal_scroll_list to enable horizontal scrolling columns and frozen row headers instead of paginated sets Addresses Issue #2272. --- app/views/rails_admin/main/index.html.haml | 123 +++++++++++++----- lib/rails_admin/config.rb | 4 + .../list/rails_admin_config_list_spec.rb | 109 ++++++++++++++++ 3 files changed, 202 insertions(+), 34 deletions(-) diff --git a/app/views/rails_admin/main/index.html.haml b/app/views/rails_admin/main/index.html.haml index 16a162a5ee..5d4fc24953 100644 --- a/app/views/rails_admin/main/index.html.haml +++ b/app/views/rails_admin/main/index.html.haml @@ -12,10 +12,22 @@ properties = @model_config.list.with(controller: self.controller, view: self, object: @abstract_model.model.new).visible_fields checkboxes = @model_config.list.checkboxes? # columns paginate - sets = get_column_sets(properties) - properties = sets[params[:set].to_i] || [] - other_left = ((params[:set].to_i - 1) >= 0) && sets[params[:set].to_i - 1].present? - other_right = sets[params[:set].to_i + 1].present? + horiz_scroll_config = RailsAdmin::Config.horizontal_scroll_list + horiz_scroll_enabled = !!horiz_scroll_config + if horiz_scroll_enabled + if horiz_scroll_config.is_a?(Hash) + horiz_scroll_frozen_cols = horiz_scroll_config[:num_frozen_columns] + horiz_scroll_css = horiz_scroll_config[:css] + end + horiz_scroll_frozen_cols ||= 3 # by default, freeze checkboxes, links & first property (usually id?) + horiz_scroll_frozen_cols -= 1 unless checkboxes + else + horiz_scroll_frozen_cols = 0 + sets = get_column_sets(properties) + properties = sets[params[:set].to_i] || [] + other_left = ((params[:set].to_i - 1) >= 0) && sets[params[:set].to_i - 1].present? + other_right = sets[params[:set].to_i + 1].present? + end - content_for :contextual_tabs do - if checkboxes @@ -39,11 +51,43 @@ jQuery(function($) { #{ordered_filter_string} }); +- if horiz_scroll_enabled && horiz_scroll_frozen_cols > 0 + :javascript + (function(){ + if (window.rails_admin_horizontal_scroll_list) { return; } // Don't add this handler multiple times. + window.rails_admin_horizontal_scroll_list = true; + var $ = jQuery; + var setFrozenColPositions = function(){ + var firstPosition, $td; + $('#bulk_form').find('table tr').each(function(index, tr){ + $(tr).find('.scroll-frozen').each(function(idx, td){ + $td = $(td); + if (idx === 0) { + firstPosition = $td.position().left; + } + td.style.left = ($td.position().left-firstPosition)+'px'; + }); + }); + }; + $(window).on('load', setFrozenColPositions); // Update after link icons load. + $(document).on('rails_admin.dom_ready', setFrozenColPositions); + }()); %style - properties.select{ |p| p.column_width.present? }.each do |property| = "#list th.#{property.css_class} { width: #{property.column_width}px; min-width: #{property.column_width}px; }" = "#list td.#{property.css_class} { max-width: #{property.column_width}px; }" + - if horiz_scroll_enabled + \.table-wrapper { margin-bottom: 20px; overflow-x: auto; } + \.table-wrapper .table { margin-bottom: 0; } + - if horiz_scroll_frozen_cols > 0 + \/* Remove transparency on frozen cells. */ + \.table-striped > tbody > tr:nth-child(even) > td.scroll-frozen, .table-striped > thead > tr > th.scroll-frozen { background-color: #fff; } + body.rails_admin .table .headerSortUp.scroll-frozen, body.rails_admin .table .headerSortDown.scroll-frozen { background-color: #e2eff6; } + \.scroll-frozen { position: sticky; } + \.scroll-frozen-last { box-shadow: -1px 0 0 0 #ddd inset; } /* border-right isn't sticky */ + \.table-condensed th.scroll-frozen-last, .table-condensed td.scroll-frozen-last { padding-right: 6px; } + = horiz_scroll_css #list = form_tag(index_path(params.except(*%w[page f query])), method: :get, class: "pjax-form form-inline") do @@ -74,37 +118,48 @@ %p %strong= description - %table.table.table-condensed.table-striped - %thead - %tr - - if checkboxes - %th.shrink - %input.toggle{type: "checkbox"} - - if other_left - %th.other.left.shrink= "..." - - properties.each do |property| - - selected = (sort == property.name.to_s) - - if property.sortable - - sort_location = index_path params.except('sort_reverse').except('page').merge(sort: property.name).merge(selected && sort_reverse != "true" ? {sort_reverse: "true"} : {}) - - sort_direction = (sort_reverse == 'true' ? "headerSortUp" : "headerSortDown" if selected) - %th{class: "#{property.sortable && "header pjax" || nil} #{sort_direction if property.sortable && sort_direction} #{property.css_class} #{property.type_css_class}", :'data-href' => (property.sortable && sort_location), rel: "tooltip", title: "#{property.hint}"}= capitalize_first_letter(property.label) - - if other_right - %th.other.right.shrink= "..." - %th.last.shrink - %tbody - - @objects.each do |object| - %tr{class: "#{@abstract_model.param_key}_row #{@model_config.list.with(object: object).row_css_class}"} + .table-wrapper + %table.table.table-condensed.table-striped + %thead + %tr + - horiz_scroll_i = horiz_scroll_frozen_cols - if checkboxes - %td= check_box_tag "bulk_ids[]", object.id, false - - if @other_left_link ||= other_left && index_path(params.except('set').merge(params[:set].to_i != 1 ? {set: (params[:set].to_i - 1)} : {})) - %td.other.left= link_to "...", @other_left_link, class: 'pjax' - - properties.map{ |property| property.bind(:object, object) }.each do |property| - - value = property.pretty_value - %td{class: "#{property.css_class} #{property.type_css_class}", title: strip_tags(value.to_s)}= value - - if @other_right_link ||= other_right && index_path(params.merge(set: (params[:set].to_i + 1))) - %td.other.right= link_to "...", @other_right_link, class: 'pjax' - %td.last.links - %ul.inline.list-inline= menu_for :member, @abstract_model, object, true + %th.shrink{class: [(horiz_scroll_i -= 1) > -1 && 'scroll-frozen', horiz_scroll_i == 0 && 'scroll-frozen-last']} + %input.toggle{type: "checkbox"} + - if horiz_scroll_enabled + %th.last.shrink{class: [(horiz_scroll_i -= 1) > -1 && 'scroll-frozen', horiz_scroll_i == 0 && 'scroll-frozen-last']} + - elsif other_left + %th.other.left.shrink= "..." + - properties.each do |property| + - selected = (sort == property.name.to_s) + - if property.sortable + - sort_location = index_path params.except('sort_reverse').except('page').merge(sort: property.name).merge(selected && sort_reverse != "true" ? {sort_reverse: "true"} : {}) + - sort_direction = (sort_reverse == 'true' ? "headerSortUp" : "headerSortDown" if selected) + %th{class: [property.sortable && "header pjax", property.sortable && sort_direction, property.css_class, property.type_css_class, (horiz_scroll_i -= 1) > -1 && 'scroll-frozen', horiz_scroll_i == 0 && 'scroll-frozen-last'], :'data-href' => (property.sortable && sort_location), rel: "tooltip", title: "#{property.hint}"}= capitalize_first_letter(property.label) + - unless horiz_scroll_enabled + - if other_right + %th.other.right.shrink= "..." + %th.last.shrink + %tbody + - @objects.each do |object| + - horiz_scroll_i = horiz_scroll_frozen_cols + %tr{class: "#{@abstract_model.param_key}_row #{@model_config.list.with(object: object).row_css_class}"} + - if checkboxes + %td{class: [(horiz_scroll_i -= 1) > -1 && 'scroll-frozen', horiz_scroll_i == 0 && 'scroll-frozen-last']}= check_box_tag "bulk_ids[]", object.id, false + - td_links = capture do + %td.last.links{class: [(horiz_scroll_i -= 1) > -1 && 'scroll-frozen', horiz_scroll_i == 0 && 'scroll-frozen-last']} + %ul.inline.list-inline= menu_for :member, @abstract_model, object, true + - if horiz_scroll_enabled + = td_links + - elsif @other_left_link ||= other_left && index_path(params.except('set').merge(params[:set].to_i != 1 ? {set: (params[:set].to_i - 1)} : {})) + %td.other.left= link_to "...", @other_left_link, class: 'pjax' + - properties.map{ |property| property.bind(:object, object) }.each do |property| + - value = property.pretty_value + %td{class: [property.css_class, property.type_css_class, (horiz_scroll_i -= 1) > -1 && 'scroll-frozen', horiz_scroll_i == 0 && 'scroll-frozen-last' ], title: strip_tags(value.to_s)}= value + - unless horiz_scroll_enabled + - if @other_right_link ||= other_right && index_path(params.merge(set: (params[:set].to_i + 1))) + %td.other.right= link_to "...", @other_right_link, class: 'pjax' + = td_links - if @model_config.list.limited_pagination .row diff --git a/lib/rails_admin/config.rb b/lib/rails_admin/config.rb index 207fa142e7..605c903c00 100644 --- a/lib/rails_admin/config.rb +++ b/lib/rails_admin/config.rb @@ -59,6 +59,9 @@ class << self # Set the max width of columns in list view before a new set is created attr_accessor :total_columns_width + # Enable horizontal-scroll table in list view, ignore total_columns_width + attr_accessor :horizontal_scroll_list + # set parent controller attr_accessor :parent_controller @@ -285,6 +288,7 @@ def reset @excluded_models = [] @included_models = [] @total_columns_width = 697 + @horizontal_scroll_list = nil @label_methods = [:name, :title] @main_app_name = proc { [Rails.application.engine_name.titleize.chomp(' Application'), 'Admin'] } @registry = {} diff --git a/spec/integration/config/list/rails_admin_config_list_spec.rb b/spec/integration/config/list/rails_admin_config_list_spec.rb index 405fa6d1fd..26104ad19a 100644 --- a/spec/integration/config/list/rails_admin_config_list_spec.rb +++ b/spec/integration/config/list/rails_admin_config_list_spec.rb @@ -474,4 +474,113 @@ end end end + + describe 'horizontal-scroll list option' do + horiz_scroll_css = { + enabled: '.table-wrapper { margin-bottom: 20px; overflow-x: auto; }', + frozen_cols: '.scroll-frozen { position: sticky; }', + last_col: '.scroll-frozen-last { box-shadow: -1px 0 0 0 #ddd inset; }', + } + js_include_test = "td.style.left = ($td.position().left-firstPosition)+'px';" + all_team_columns = ['', '', 'Id', 'Created at', 'Updated at', 'Division', 'Name', 'Logo url', 'Team Manager', 'Ballpark', 'Mascot', 'Founded', 'Wins', 'Losses', 'Win percentage', 'Revenue', 'Color', 'Custom field', 'Main Sponsor', 'Players', 'Some Fans', 'Comments'] + + it "displays all fields with on one page when true" do + RailsAdmin.config do |config| + config.horizontal_scroll_list = true + end + FactoryGirl.create_list :team, 3 + visit index_path(model_name: 'team') + cols = all('th').collect(&:text) + expect(cols[0..4]).to eq(all_team_columns[0..4]) + expect(cols).to contain_exactly(*all_team_columns) + expect(page).to have_selector('.table-wrapper') + expect(page.html).to include(horiz_scroll_css[:enabled]) + expect(page.html).to include(horiz_scroll_css[:frozen_cols]) + expect(page.html).to include(horiz_scroll_css[:last_col]) + expect(page.html).to include(js_include_test) + expect(all('.scroll-frozen').count).to eq(12) + expect(all('th.scroll-frozen').count).to eq(3) + expect(all('td.scroll-frozen').count).to eq(9) + expect(all('.scroll-frozen-last').count).to eq(4) + end + + it "displays all fields with custom css" do + custom_css = '.scroll-frozen-last { box-shadow: none; }' + RailsAdmin.config do |config| + config.horizontal_scroll_list = {css: custom_css} + end + visit index_path(model_name: 'team') + cols = all('th').collect(&:text) + expect(cols[0..4]).to eq(all_team_columns[0..4]) + expect(cols).to contain_exactly(*all_team_columns) + expect(page.html).to include(horiz_scroll_css[:enabled]) + expect(page.html).to include(horiz_scroll_css[:frozen_cols]) + expect(page.html).to include(custom_css) + expect(page.html).to include(js_include_test) + end + + it "displays all fields with custom frozen columns" do + RailsAdmin.config do |config| + config.horizontal_scroll_list = {num_frozen_columns: 2} + end + FactoryGirl.create_list :team, 3 + visit index_path(model_name: 'team') + cols = all('th').collect(&:text) + expect(cols[0..4]).to eq(all_team_columns[0..4]) + expect(cols).to contain_exactly(*all_team_columns) + expect(page.html).to include(horiz_scroll_css[:enabled]) + expect(page.html).to include(horiz_scroll_css[:frozen_cols]) + expect(page.html).to include(horiz_scroll_css[:last_col]) + expect(page.html).to include(js_include_test) + expect(all('.scroll-frozen').count).to eq(8) + expect(all('th.scroll-frozen').count).to eq(2) + expect(all('td.scroll-frozen').count).to eq(6) + expect(all('.scroll-frozen-last').count).to eq(4) + end + + it "displays all fields with no checkboxes" do + RailsAdmin.config do |config| + config.horizontal_scroll_list = true + end + RailsAdmin.config Team do + list do + checkboxes false + end + end + FactoryGirl.create_list :team, 3 + visit index_path(model_name: 'team') + cols = all('th').collect(&:text) + expect(cols[0..3]).to eq(all_team_columns[1..4]) + expect(cols).to contain_exactly(*all_team_columns[1..-1]) + expect(all('.scroll-frozen').count).to eq(8) + expect(all('th.scroll-frozen').count).to eq(2) + expect(all('td.scroll-frozen').count).to eq(6) + expect(all('.scroll-frozen-last').count).to eq(4) + end + + it "displays all fields with no frozen columns" do + RailsAdmin.config do |config| + config.horizontal_scroll_list = {num_frozen_columns: 0} + end + FactoryGirl.create_list :team, 3 + visit index_path(model_name: 'team') + cols = all('th').collect(&:text) + expect(cols[0..4]).to eq(all_team_columns[0..4]) + expect(cols).to contain_exactly(*all_team_columns) + expect(page.html).to include(horiz_scroll_css[:enabled]) + expect(page.html).not_to include(horiz_scroll_css[:frozen_cols]) + expect(page.html).not_to include(js_include_test) + expect(all('.scroll-frozen').count).to eq(0) + expect(all('.scroll-frozen-last').count).to eq(0) + end + + it "displays sets when not set" do + visit index_path(model_name: 'team') + expect(all('th').collect(&:text)).to eq ['', 'Id', 'Created at', 'Updated at', 'Division', 'Name', 'Logo url', '...', ''] + expect(page.html).not_to include(horiz_scroll_css[:enabled]) + expect(page.html).not_to include(horiz_scroll_css[:frozen_cols]) + expect(page.html).not_to include(horiz_scroll_css[:last_col]) + expect(page.html).not_to include(js_include_test) + end + end end