...

Пагинация коробочных таблиц с возможностью редактирования ячеек

Тема в разделе "Примеры решений и дополнительных модулей", создана пользователем luppov, 3 май 2024.

  1. luppov

    luppov Новичок

    Как известно для коробочных таблиц с большим объемом строк, чтобы избежать долгой загрузки страниц или форм требуется "Пагинация".

    Решение:
    Пример основан на создании двух одинаковых таблиц, одна из которых выступает в качестве хранилища данных, а вторая для постраничного рендера на форму. В качестве формы будет использована "Форма задачи в БП"
    В данном примере отсутствует обработчик события таблицы, я пошел другим путем чтобы уйти от различных неприятностей в виде рекурсии, а именно при пагинации вызывается метод который перезаписывает данные в строках "Таблицы хранилища".
    1. Создадим две одинаковых таблицы в контексте процесса "json_table - таблица хранилище" ,"table - таблица для рендера на форму" [​IMG]
    2. Напишем сценарий БП для заполнения таблицы-хранилища данными [​IMG]
      Код:
      
      /**
      Here you can write scripts for complex server processing of the context during process execution.
      To write scripts, use TypeScript (https://www.typescriptlang.org).
      ELMA365 SDK documentation available on https://tssdk.elma365.com.
      **/

      // Метод формирования uuid для строк таблицы, если есть возможность лучше воспользоваться библиотекой npm i uuid
      function ID_generation(): string {
          const 
      valid "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNOPQRSTUVWXYZ23456789!&?`~№;:^\|/'#$%&*=-+_)(}{][@";
          
      let length 15
          let id 
      ''
          
      while (length--) {
              
      id += valid.charAt(Math.floor(Math.random() * valid.length))
          }

          return 
      id
      }

      // Метод заполнит тестовыми данными таблицу и проставит uuid для каждой строки
      async function fillTableJson(): Promise<void> {
          const 
      users await Context.fields.users.app.search().where((fg) => g.and(
              
      f.__deletedAt.eq(null),
          )).
      size(10000).all()

          if (!
      users || !users.length) return

          
      let counter 1
          users
      .forEach(user => {
              const 
      row Context.data.json_table!.insert()
              
      row.uuid ID_generation()
              
      row.name = `Name ${counter}`
              
      row.user user
              row
      .users = [user]
              
      counter++
          })

          
      Context.data.json_table Context.data.json_table
      }
    3. Добавим блок задачи в БП и создадим форму в расширенном режиме [​IMG]
    4. Вынесем таблицу на форму и добавим ВКод (P.S. пагинация написана мной, не обязательно использовать именно ее как единственный правильный вариант, возможно вам будет быстрее воспользоваться какой-либо библиотекой, для меня это был наиболее быстрый вариант так как уже была заготовка)[​IMG] Теперь по порядку ВКод:

      -
      Пагинация HTML
      Код:
      
      <div class="castom-pagination__wrapper"></div>
      - Пагинация CSS
      Код:
      
      <style>
          .
      castom-pagination__wrapper {
              
      width100%;
              
      max-width526px;
              
      displayflex;
              
      align-itemscenter;
              
      justify-contentend;
              
      padding14px 1px 5px 14px;
          }

          .
      castom-paginations {
              
      displayflex;
              
      align-itemscenter;
              
      gap5px;
              
      z-index100;
          }

          .
      castom-paginations__button-prev {
              
      font-size14px;
              
      font-weight500;
              
      bordernone;
              
      border-radius2px;
              
      padding3px 5px;
              
      background-color#d9d9d9;
              
      opacity0.98;
              
      z-index100;
              -
      webkit-box-shadow0px 5px 10px 2px rgba(3460800.2);
              -
      moz-box-shadow0px 5px 10px 2px rgba(3460800.2);
              
      box-shadow0px 5px 10px 2px rgba(3460800.2);
              
      cursorpointer;
              
      transitionall 0.3s;
          }

          .
      castom-paginations__button-next {
              
      font-size14px;
              
      font-weight500;
              
      bordernone;
              
      border-radius2px;
              
      padding3px 5px;
              
      background-color#d9d9d9;
              
      opacity0.98;
              
      z-index100;
              -
      webkit-box-shadow0px 5px 10px 2px rgba(3460800.2);
              -
      moz-box-shadow0px 5px 10px 2px rgba(3460800.2);
              
      box-shadow0px 5px 10px 2px rgba(3460800.2);
              
      cursorpointer;
              
      transitionall 0.3s;
          }

          .
      castom-paginations__button-prev:active {
              
      background-color#5081e5;
              
      -webkit-box-shadow5px 5px 5px -5px rgba(3460800.6inset;
              -
      moz-box-shadow5px 5px 5px -5px rgba(3460800.6inset;
              
      box-shadow5px 5px 5px -5px rgba(3460800.6inset;
          }

          .
      castom-paginations__button-next:active {
              
      background-color#5081e5;
              
      -webkit-box-shadow5px 5px 5px -5px rgba(3460800.6inset;
              -
      moz-box-shadow5px 5px 5px -5px rgba(3460800.6inset;
              
      box-shadow5px 5px 5px -5px rgba(3460800.6inset;
          }

          .
      castom-paginations__list {
              
      displayflex;
              list-
      style-typenone;
              
      gap5px;
              
      margin0;
              
      padding0;

          }

          .
      castom-paginations__list-item {
              
      z-index100;
              
      displayflex;
          }

          .
      castom-paginations__list-button-page {
              
      font-size14px;
              
      font-weight500;
              
      colorblack;
              
      bordernone;
              
      border-radius2px;
              
      cursorpointer;
              
      padding3px 5px;
              
      background-color#d9d9d9;
              
      opacity0.98;
              
      z-index100;
              -
      webkit-box-shadow0px 5px 10px 2px rgba(3460800.2);
              -
      moz-box-shadow0px 5px 10px 2px rgba(3460800.2);
              
      box-shadow0px 5px 10px 2px rgba(3460800.2);
          }

          .
      castom-paginations__list-button-page:hover {
              
      opacity0.7;
          }

          .
      castom-paginations__list-button-page--active {
              
      background-color#5081e5;
              
      color#fff;
              
      -webkit-box-shadow5px 5px 5px -5px rgba(3460800.6inset;
              -
      moz-box-shadow5px 5px 5px -5px rgba(3460800.6inset;
              
      box-shadow5px 5px 5px -5px rgba(3460800.6inset;
          }



          .
      castom-paginations__list-item--hide {
              
      displaynone;
              
      width0;
          }

          .
      castom-paginations__list-item--one-page::after {
              
      content'...';
              
      text-aligncenter;
              
      displayflex;
              
      align-itemscenter;
              
      padding0 5px 0 5px;
          }

          .
      castom-paginations__list-item--end-page::before {
              
      content'...';
              
      text-aligncenter;
              
      displayflex;
              
      align-itemsend;
              
      padding0 5px 0 0;
          }

          @
      media(max-width:768px) {
              .
      castom-paginations__list-button-page {
                  
      font-size16px;
              }
          }
      </
      style>
      - Пагинация JS
      Код:
      
      <script>
         
      function 
      createClassPagination(propsForPagination) {
      class 
      Pagination {
        
      constructor(props) {
         
      this.props props
        
      }

        
      addCastomSelectorStyleMod() {
         
      // кастомные стили к пагинации тегу nav
         
      this.castomSelectorPagination =
          
      this.pagination.classList[0] + '--' this.props.castomSelectorMod
         this
      .pagination.classList.add(this.castomSelectorPagination)

         
      // кастомный селектор к тегу ul
         
      this.castomSelectorListPages =
          
      this.paginationListPages.classList[0] + '--' this.props.castomSelectorMod
         this
      .paginationListPages.classList.add(this.castomSelectorListPages)

         
      // кастомный селектор к тегам li
         
      if (this.listItemsAll[0]) {
          
      this.castomSelectorListItem =
           
      this.listItemsAll[0].classList[0] + '--' this.props.castomSelectorMod

          this
      .listItemsAll.forEach((item) => {
           
      item.classList.add(this.castomSelectorListItem)
          })
         }

         
      // кастомный селектор к тегам button
         
      if (this.buttonsAll[0]) {
          
      this.castomSelectorButton =
           
      this.buttonsAll[0].classList[0] + '--' this.props.castomSelectorMod

          this
      .buttonsAll.forEach((button) => {
           
      button.classList.add(this.castomSelectorButton)
          })
         }
        }

        
      // метод отрисовывает пагинацию
        
      init() {
         if (!
      this.props.countPages || this.props.countPages === || !this.props.selectorForWrapper)
          return

         if (!
      this.props.range || this.props.range === || this.props.range 3) {
          
      this.props.range 5
         
      }
         
      this.pagination document.createElement('nav')
         
      this.pagination.classList.add('castom-paginations')

         
      this.paginationListPages document.createElement('ul')
         
      this.paginationListPages.classList.add('castom-paginations__list')

         
      this.pagination.append(this.paginationListPages)

         
      this.renderPages()

         
      this.wrapper document.querySelector('.' this.props.selectorForWrapper)
         
      this.wrapper.append(this.pagination)

         
      // инициализируем listItemsAll для дальнейшего использования в методах класса
         
      this.listItemsAll = Array.from(
          
      this.pagination.querySelectorAll('.castom-paginations__list-item')
         )

         
      // инициализируем buttonAll для дальнейшего использования в методах класса
         
      this.buttonsAll this.pagination.querySelectorAll('.castom-paginations__list-button-page')

         
      this.handlers()
         
      this.nextRanderPages()
         if (
      this.props.castomSelectorMod) {
          
      this.addCastomSelectorStyleMod()
         }
        }

        
      // метод рендерит страницы при инициализации(используется в init)
        
      renderPages() {
         for (
      let i 1<= this.props.countPagesi++) {
          
      this.listItem document.createElement('li')
          
      this.listItem.classList.add('castom-paginations__list-item')

          
      this.buttonPage document.createElement('button')
          
      this.buttonPage.setAttribute('data-index'i// присваиваем data атрибут для определения страниц
          
      this.buttonPage.classList.add('castom-paginations__list-button-page')

          if (
      this.props.startPage && this.props.startPage === i) {
           
      this.buttonPage.classList.add('castom-paginations__list-button-page--active')
          }
          if (
      this.props.range && !== this.props.countPages) {
           
      this.listItem.classList.add('castom-paginations__list-item--hide')
          }

          if (
      === this.props.countPages && this.props.countPages 5) {
           
      this.listItem.classList.add('castom-paginations__list-item--end-page')
          }

          
      this.buttonPage.type 'button'
          
      this.buttonPage.textContent i

          this
      .listItem.append(this.buttonPage)
          
      this.paginationListPages.append(this.listItem)
         }
        }

        
      // метод сбрасывает на 1 страницу
        
      reset() {
         
      this.wrapper.textContent ''
         
      this.props.startPage 1
         this
      .init()
        }

        
      //метод очищает страницы пагинации
        
      removeRenderPages() {
         
      this.listItemsAll.forEach((itemidx) => {
          if (
      idx !== && idx !== this.props.countPages 1)
           
      item.classList.add('castom-paginations__list-item--hide')
         })
        }

        
      // метод рендерит страницу при клике на соответстующий номер
        
      nextRanderPages() {
         if (
      this.props.countPages 2) return

         
      // Ecли клик по странице с индексом меньше, чем колличество отображемых страниц
         
      if (this.props.startPage this.props.range) {
          
      this.removeRenderPages()
          
      this.listItemsAll.forEach((itemidx) => {
           if (
      idx <= this.props.range 1) {
            
      item.classList.remove('castom-paginations__list-item--hide')
           }
          })
          
      this.listItemsAll[0].classList.remove('castom-paginations__list-item--one-page')
          if (
      this.props.countPages 5) {
           
      this.listItemsAll[this.props.countPages 1].classList.add(
            
      'castom-paginations__list-item--end-page'
           
      )
          }
          return
         }

         
      // Ecли клик по последней странице или меньше последней, но болше чем кол-во страниц минус кол-во отображаемых страниц
         
      if (
          
      this.props.startPage <= this.props.countPages &&
          
      this.props.startPage this.props.countPages this.props.range &&
          
      this.props.startPage !== this.props.countPages this.props.range 1
         
      ) {
          
      this.removeRenderPages()

          
      this.listItemsAll.forEach((itemidx) => {
           if (
      idx this.props.countPages && idx this.props.countPages this.props.range 1) {
            
      item.classList.remove('castom-paginations__list-item--hide')
           }
          })
          
      this.listItemsAll[this.props.countPages 1].classList.remove(
           
      'castom-paginations__list-item--end-page'
          
      )
          if (
      this.props.countPages 5) {
           
      this.listItemsAll[0].classList.add('castom-paginations__list-item--one-page')
          }
          return
         }

         
      // В остальных случаях при клике на страницу
         
      this.removeRenderPages()

         
      // добавить многоточие после первой и перед последней страницей
         
      if (this.props.countPages 5) {
          
      this.listItemsAll[0].classList.add('castom-paginations__list-item--one-page')
          
      this.listItemsAll[this.props.countPages 1].classList.add(
           
      'castom-paginations__list-item--end-page'
          
      )
         }

         for (
      let i this.props.startPagethis.props.startPage this.props.range 1i--) {
          
      this.listItemsAll[i].classList.remove('castom-paginations__list-item--hide')
         }

         for (
      let i this.props.startPagethis.props.startPage this.props.range 1i++) {
          
      this.listItemsAll[i].classList.remove('castom-paginations__list-item--hide')
         }
        }

        
      handlers() {
         
      this.pagination.addEventListener('click', (e) => {
          if (
      e.target.closest('.castom-paginations__list-button-page')) {
           
      this.buttonsAll.forEach((page) => {
            if (
      page.closest('.castom-paginations__list-button-page--active')) {
             
      page.classList.remove('castom-paginations__list-button-page--active')
            }
           })

           
      e.target.classList.add('castom-paginations__list-button-page--active')
           
      this.props.startPage = +e.target.dataset.index

           this
      .nextRanderPages() // перерисовывает пагинацию относительно выбранной страницы
           
      this.props.callback(this.props.startPage// вызываем callback переданный из пропсов (должен принимать номер страницы)
          
      }
         })
        }
      }

      return new 
      Pagination(propsForPagination)
      }


      function 
      insertPagination(quantityPages,call) {
         
          if (!
      quantityPages || quantityPages === || isNaN(quantityPages)) return
         
          const 
      propsForPagination = {
              
      selectorForWrapper'castom-pagination__wrapper',
              
      countPagesquantityPages// кол-во страниц
              
      range5// сколько страниц отображать
              
      startPage1// номер активной страницы
              
      callbackcall// callback function принимает номер страницы по которой выполнен click
          
      }

          const 
      pagination createClassPagination(propsForPagination)
          
      pagination.init()

      }

      function 
      clearPagination () {
        
      document.querySelector('.castom-pagination__wrapper').textContent ''
      }

      </script>
      - ВКод Отрисовать пагинацию
      Код:
      
      <script>
          <%= 
      Scripts %>.initTable()  
      </script>
    5. В контекст формы добавим переменную с номером последней строки, которую будем получать динамически и скрывать последнюю строку таблицы стилями[​IMG] Так выглядит ВКод со стилями который скроет последнюю строку таблицы
      Код:
      
        
      <style>
          .
      target-table elma-type-table-full-line[data-index="<%= ViewContext.data.last_line_number %>"] {
              
      displaynone;
          }
      </
      style>

    6. Клиентский сценарий
      Код:
      
      /* Client scripts module */
      declare const consoleany
      declare const insertPaginationany
      declare const clearPaginationany

      const quantityItemsInTable 20 // Кол-во элементов таблицы на 1 странице


      // Что бы уйти от различных setTimeout initTable вызывается в ВКод - е
      async function initTable() {
          if (!
      Context.data.json_table || !Context.data.json_table.length) return
          const 
      quantityPages Math.ceil(Context.data.json_table.length quantityItemsInTable// Кол-во страниц для пагинации
          
      insertPagination(quantityPageschangePage// Отрисовать пагинацию
          
      fillTable(1// Заполнить таблицу
          
      setLastLineNumber() // Установить номер последней строки таблицы
      }


      // callback который передается в инстанс пагинации для листания по страницам
      async function changePage(numPagenumber) {
          
      saveChangesToJsonTable() // Сохранить данные в таблице хранилище

          
      Context.data.table Context.fields.table.create()  // Создать заного новый экземпляр таблицы

          
      fillTable(numPage// Заполнить таблицу
          
      setLastLineNumber() // Установить номер последней строки таблицы (в ВКод будем скрывать ее)
      }

      function 
      setLastLineNumber() {
          if (!
      Context.data.table || !Context.data.table.length) return
          
      ViewContext.data.last_line_number Context.data.table.length 1
      }

      async function saveChangesToJsonTable() {
          if (!
      Context.data.table || !Context.data.table.length || !Context.data.json_table || !Context.data.json_table.length) return
          for (
      let i 0Context.data.table.lengthi++) {
              const 
      findRowToJasonTable Context.data.json_table!.find(row => row.uuid === Context.data.table![i].uuid)

              if (!
      findRowToJasonTable) continue
             
              
      findRowToJasonTable.name Context.data.table![i].name
              findRowToJasonTable
      .user Context.data.table![i].user
              findRowToJasonTable
      .users Context.data.table![i].users
          
      }
          
      Context.data.json_table Context.data.json_table
      }

      // Функция заполняет таблицу данными
      function fillTable(pagenumber) {
          if (!
      Context.data.json_table || !Context.data.json_table.length) return

          const 
      startRange page quantityItemsInTable quantityItemsInTable // C какого кол-ва элементов отрисовать таблицу
          
      const endRange page quantityItemsInTable // По какое кол-во элементов отрисовать таблицу

          
      for (let i startRangeendRangei++) {
              if (
      === Context.data.json_table.length) break
              const 
      row Context.data.table!.insert();
              
      /*
               Тут не стал писать проверку на то что изменились данные или же нет, на мой взгляд проверка может оказаться трудозатратнее чем без нее,
               через JSON.stringify(data) === JSON.stringify(newData) считаю не надежным, при смене ключей в объекте местами это не сработает.
              */
              
      row.uuid Context.data.json_table[i].uuid
              row
      .name Context.data.json_table[i].name
              row
      .user Context.data.json_table[i].user
              row
      .users Context.data.json_table[i].users

          
      }

          
      Context.data.table Context.data.table
      }
    Результат:
    [​IMG]
    Вывод:

    • Написан простой вариант редактируемой таблицы с пагинацией без обработчиков событий и различных костылей в виде "setTimeout"
    • Таблица почти с тысячей элементов разбита постранично и не забивает так сильно стэк задачами рендера, что дает нормальную загрузку формы.
    Последнее редактирование: 15 июл 2024