diff --git a/app/assets/javascripts/projects/index.js b/app/assets/javascripts/projects/index.js index 1049ca2c4..cc843cfed 100644 --- a/app/assets/javascripts/projects/index.js +++ b/app/assets/javascripts/projects/index.js @@ -7,7 +7,7 @@ // - refresh project users tab after manage user modal is closed // - refactor view handling using library, ex. backbone.js -/* global HelperModule dropdownSelector Sidebar Turbolinks filterDropdown */ +/* global HelperModule dropdownSelector Sidebar Turbolinks filterDropdown InfiniteScroll */ var ProjectsIndex = (function() { const PERMISSIONS = ['editable', 'archivable', 'restorable', 'moveable', 'deletable']; @@ -16,6 +16,7 @@ var ProjectsIndex = (function() { var cardsWrapper = '#cardsWrapper'; var editProjectModal = '#edit-modal'; var moveToModal = '#move-to-modal'; + var pageSize = 20; var exportProjectsModal = null; var exportProjectsModalHeader = null; @@ -443,23 +444,35 @@ var ProjectsIndex = (function() { }); } + function loadPlaceHolder() { + let palceholder = ''; + $.each(Array(pageSize), function() { + palceholder += $('#projectPlaceholder').html(); + }); + $(palceholder).insertAfter($(cardsWrapper).find('.table-header')); + } + function loadCardsView() { + var requestParams = { + view_mode: $('.projects-index').data('view-mode'), + sort: projectsCurrentSort, + search: projectsViewSearch, + members: membersFilter, + created_on_from: createdOnFromFilter, + created_on_to: createdOnToFilter, + folders_search: lookInsideFolders, + archived_on_from: archivedOnFromFilter, + archived_on_to: archivedOnToFilter + }; var viewContainer = $(cardsWrapper); + var cardsUrl = viewContainer.data('projects-cards-url'); + + loadPlaceHolder(); $.ajax({ - url: viewContainer.data('projects-cards-url'), + url: cardsUrl, type: 'GET', dataType: 'json', - data: { - view_mode: $('.projects-index').data('view-mode'), - sort: projectsCurrentSort, - search: projectsViewSearch, - members: membersFilter, - created_on_from: createdOnFromFilter, - created_on_to: createdOnToFilter, - folders_search: lookInsideFolders, - archived_on_from: archivedOnFromFilter, - archived_on_to: archivedOnToFilter - }, + data: { ...requestParams, ...{ page: 1 } }, success: function(data) { $('#breadcrumbsWrapper').html(data.breadcrumbs_html); $(projectsWrapper).find('.projects-title').html(data.title); @@ -474,6 +487,23 @@ var ProjectsIndex = (function() { selectedProjects.length = 0; selectedProjectFolders.length = 0; updateProjectsToolbar(); + if (data.filtered) { + InfiniteScroll.removeScroll(cardsWrapper); + } else { + InfiniteScroll.init(cardsWrapper, { + url: cardsUrl, + eventTarget: window, + placeholderTemplate: '#projectPlaceholder', + endOfListTemplate: '#projectEndOfList', + pageSize: pageSize, + customResponse: (response) => { + $(response.cards_html).appendTo(cardsWrapper); + }, + customParams: (params) => { + return { ...params, ...requestParams }; + } + }); + } }, error: function() { viewContainer.html('Error loading project list'); @@ -490,6 +520,7 @@ var ProjectsIndex = (function() { $(projectsPageSelector).find('.cards-switch .button-to').removeClass('selected'); $(ev.target).find('.button-to').addClass('selected'); $(ev.target).parents('.dropdown.view-switch').removeClass('open'); + InfiniteScroll.loadMore(cardsWrapper); }) .on('ajax:error', '.change-projects-view-type-form', function(ev, data) { HelperModule.flashAlertMsg(data.responseJSON.flash, 'danger'); diff --git a/app/assets/javascripts/sitewide/infinite_scroll.js b/app/assets/javascripts/sitewide/infinite_scroll.js index 12a3aef4e..94abdadab 100644 --- a/app/assets/javascripts/sitewide/infinite_scroll.js +++ b/app/assets/javascripts/sitewide/infinite_scroll.js @@ -6,7 +6,8 @@ var InfiniteScroll = (function() { } function scrollNotVisible($container) { - return (getScrollHeight($container) - $container.height() - 150 <= 0); + let eventTarget = $($container.data('config').eventTarget || $container); + return scrollHitBottom(eventTarget[0]); } function loadData($container, page = 1) { @@ -15,8 +16,9 @@ var InfiniteScroll = (function() { if ($container.hasClass('loading') || $container.hasClass('last-page')) return; $container.addClass('loading'); - + renderPlaceholder($container); $.get($container.data('config').url, params, function(result) { + $container.find('.placeholder-block').remove(); if ($container.data('config').customResponse) { $container.data('config').customResponse(result, $container); } else { @@ -27,6 +29,9 @@ var InfiniteScroll = (function() { $container.data('next-page', result.next_page); } else { $container.addClass('last-page'); + if ($container.data('config').endOfListTemplate) { + $($($container.data('config').endOfListTemplate).html()).appendTo($container); + } } $container.removeClass('loading'); @@ -44,20 +49,73 @@ var InfiniteScroll = (function() { var $container = $(object); $container.data('next-page', 2); $container.data('config', config); - if (config.loadFirstPage) { loadData($container, 1); } - $container.on('scroll', () => { - if ($container.scrollTop() + $container.height() > getScrollHeight($container) - 150 && !$container.hasClass('last-page')) { + let eventTarget = $($container.data('config').eventTarget || $container); + eventTarget.on('scroll', () => { + if (scrollHitBottom(eventTarget[0]) && !$container.hasClass('last-page')) { loadData($container, $container.data('next-page')); } }); + + if (scrollNotVisible($container)) { + loadData($container, $container.data('next-page')); + } + } + + // support functions + + // Full scroll height + function scrollHeight(con) { + return con.scrollHeight || document.documentElement.scrollHeight; + } + + // Top scroll position + function scrollTop(con) { + return con.scrollTop || con.scrollY || 0; + } + + // Get container size + function containerSize(con) { + return con.innerHeight || con.offsetHeight; + } + + // Container position + function containerPosition(con) { + return scrollTop(con) + containerSize(con); + } + + // Check when load next page + function scrollHitBottom(con) { + return scrollHeight(con) - containerPosition(con) <= 0; + } + + function removeScroll(con) { + let $container = $(con); + + if (!$container.data('config')) { + return; + } + + let eventTarget = $($container.data('config').eventTarget) || $container; + $container.data('config', null); + $container.data('next-page', null); + eventTarget.off('scroll'); + } + + function renderPlaceholder($container) { + let palceholder = ''; + $.each(Array($container.data('config').pageSize || 10), function() { + palceholder += $($container.data('config').placeholderTemplate).html(); + }); + $(palceholder).addClass('placeholder-block').appendTo($container); } return { init: (object, config) => { + removeScroll(object); initScroll(object, config); }, resetScroll: (object) => { @@ -65,6 +123,15 @@ var InfiniteScroll = (function() { if (scrollNotVisible($(object))) { loadData($(object), $(object).data('next-page')); } + }, + removeScroll: (object) => { + removeScroll(object); + }, + loadMore: (object) => { + let $container = $(object); + if (scrollNotVisible($container)) { + loadData($container, $container.data('next-page')); + } } }; }()); diff --git a/app/assets/stylesheets/projects.scss b/app/assets/stylesheets/projects.scss index 756c5fbce..9f06f1983 100644 --- a/app/assets/stylesheets/projects.scss +++ b/app/assets/stylesheets/projects.scss @@ -623,6 +623,11 @@ li.module-hover { --card-min-width: 291px; --list-columns-number: 6; + &.last-page { + position: relative; + padding-bottom: 5em; + } + .projects-group { grid-column: 1/-1; margin: 0; @@ -784,6 +789,58 @@ li.module-hover { } } } + + &.project-placeholder { + align-items: center; + background-color: $color-white; + border-radius: $border-radius-default; + box-shadow: $flyout-shadow; + display: flex; + + .placeholder-element { + animation-name: placeholder-pulsing; + animation-duration: 2s; + animation-iteration-count: infinite; + background-color: $color-alto; + border-radius: $border-radius-default; + height: 18px; + + &.line-0 { + flex-basis: 80%; + } + + &.line-1 { + flex-basis: 65%; + } + + &.line-2 { + flex-basis: 85%; + } + + &.line-3 { + flex-basis: 25%; + } + + &.circle { + border-radius: 50%; + height: 27px; + margin-left: .5em; + width: 27px; + } + + @keyframes placeholder-pulsing { + 0% { + opacity: 1; + } + 50% { + opacity: .5; + } + 100% { + opacity: 1; + } + } + } + } } &.list { @@ -884,9 +941,59 @@ li.module-hover { position: initial; } } + + &.project-placeholder { + display: contents; + + .placeholder-element { + display: none; + padding: .5em 0; + } + + .line-0 { + display: block; + grid-column: 2; + width: 50%; + } + + .line-1 { + display: block; + grid-column: 3; + width: 75%; + } + + .line-2 { + display: block; + grid-column: 4; + width: 75%; + } + + .circle-0 { + display: block; + grid-column: 5; + } + } } } } + + .project-list-end-placeholder { + align-items: center; + background-color: $color-concrete; + bottom: 1em; + display: flex; + height: 3em; + left: calc(50% - 150px); + margin: 0 auto; + padding: 1em; + position: absolute; + width: 300px; + + > * { + flex-grow: 1; + text-align: center; + } + } } &.active { diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 7f70e2dac..afd95fac1 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -39,6 +39,7 @@ class ProjectsController < ApplicationController if filters_included? render json: { toolbar_html: render_to_string(partial: 'projects/index/toolbar.html.erb'), + filtered: true, cards_html: render_to_string( partial: 'projects/index/team_projects_grouped_by_folder.html.erb', locals: { projects_by_folder: overview_service.grouped_by_folder_project_cards } @@ -63,14 +64,18 @@ class ProjectsController < ApplicationController projects_cards_url = cards_projects_url end + cards = Kaminari.paginate_array(overview_service.project_and_folder_cards) + .page(params[:page] || 1).per(20) + render json: { projects_cards_url: projects_cards_url, breadcrumbs_html: breadcrumbs_html, title: title, + next_page: cards.next_page, toolbar_html: render_to_string(partial: 'projects/index/toolbar.html.erb'), cards_html: render_to_string( partial: 'projects/index/team_projects.html.erb', - locals: { cards: overview_service.project_and_folder_cards } + locals: { cards: cards } ) } end diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb index 41344a0e8..3c948cb1f 100644 --- a/app/views/projects/index.html.erb +++ b/app/views/projects/index.html.erb @@ -38,4 +38,23 @@ + + + <% 4.times do |i| %> + + <% end %> + <% 3.times do |i| %> + + <% end %> + + + + + + + <%= t('.end_of_list_placeholder') %> + + + + <%= javascript_include_tag "projects/index" %> diff --git a/config/locales/en.yml b/config/locales/en.yml index e899a807d..2618fae9c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -419,6 +419,7 @@ en: users: "Members" name: "Project name" archived_date: "Archived" + end_of_list_placeholder: 'You’ve reached the end of the list' folder: description: "%{projects_count} projects | %{folders_count} folders" modal_new_project_folder: