...

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

Тема в разделе "Компоненты и интерфейс", создана пользователем 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 (0 < 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((f, g) => 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 {
              
      width: 100%;
              
      max-width: 526px;
              
      display: flex;
              
      align-items: center;
              
      justify-content: end;
              
      padding: 14px 1px 5px 14px;
          }

          .
      castom-paginations {
              
      display: flex;
              
      align-items: center;
              
      gap: 5px;
              
      z-index: 100;
          }

          .
      castom-paginations__button-prev {
              
      font-size: 14px;
              
      font-weight: 500;
              
      border: none;
              
      border-radius: 2px;
              
      padding: 3px 5px;
              
      background-color: #d9d9d9;
              
      opacity: 0.98;
              
      z-index: 100;
              -
      webkit-box-shadow: 0px 5px 10px 2px rgba(34, 60, 80, 0.2);
              -
      moz-box-shadow: 0px 5px 10px 2px rgba(34, 60, 80, 0.2);
              
      box-shadow: 0px 5px 10px 2px rgba(34, 60, 80, 0.2);
              
      cursor: pointer;
              
      transition: all 0.3s;
          }

          .
      castom-paginations__button-next {
              
      font-size: 14px;
              
      font-weight: 500;
              
      border: none;
              
      border-radius: 2px;
              
      padding: 3px 5px;
              
      background-color: #d9d9d9;
              
      opacity: 0.98;
              
      z-index: 100;
              -
      webkit-box-shadow: 0px 5px 10px 2px rgba(34, 60, 80, 0.2);
              -
      moz-box-shadow: 0px 5px 10px 2px rgba(34, 60, 80, 0.2);
              
      box-shadow: 0px 5px 10px 2px rgba(34, 60, 80, 0.2);
              
      cursor: pointer;
              
      transition: all 0.3s;
          }

          .
      castom-paginations__button-prev:active {
              
      background-color: #5081e5;
              
      -webkit-box-shadow: 5px 5px 5px -5px rgba(34, 60, 80, 0.6) inset;
              -
      moz-box-shadow: 5px 5px 5px -5px rgba(34, 60, 80, 0.6) inset;
              
      box-shadow: 5px 5px 5px -5px rgba(34, 60, 80, 0.6) inset;
          }

          .
      castom-paginations__button-next:active {
              
      background-color: #5081e5;
              
      -webkit-box-shadow: 5px 5px 5px -5px rgba(34, 60, 80, 0.6) inset;
              -
      moz-box-shadow: 5px 5px 5px -5px rgba(34, 60, 80, 0.6) inset;
              
      box-shadow: 5px 5px 5px -5px rgba(34, 60, 80, 0.6) inset;
          }

          .
      castom-paginations__list {
              
      display: flex;
              list-
      style-type: none;
              
      gap: 5px;
              
      margin: 0;
              
      padding: 0;

          }

          .
      castom-paginations__list-item {
              
      z-index: 100;
              
      display: flex;
          }

          .
      castom-paginations__list-button-page {
              
      font-size: 14px;
              
      font-weight: 500;
              
      color: black;
              
      border: none;
              
      border-radius: 2px;
              
      cursor: pointer;
              
      padding: 3px 5px;
              
      background-color: #d9d9d9;
              
      opacity: 0.98;
              
      z-index: 100;
              -
      webkit-box-shadow: 0px 5px 10px 2px rgba(34, 60, 80, 0.2);
              -
      moz-box-shadow: 0px 5px 10px 2px rgba(34, 60, 80, 0.2);
              
      box-shadow: 0px 5px 10px 2px rgba(34, 60, 80, 0.2);
          }

          .
      castom-paginations__list-button-page:hover {
              
      opacity: 0.7;
          }

          .
      castom-paginations__list-button-page--active {
              
      background-color: #5081e5;
              
      color: #fff;
              
      -webkit-box-shadow: 5px 5px 5px -5px rgba(34, 60, 80, 0.6) inset;
              -
      moz-box-shadow: 5px 5px 5px -5px rgba(34, 60, 80, 0.6) inset;
              
      box-shadow: 5px 5px 5px -5px rgba(34, 60, 80, 0.6) inset;
          }



          .
      castom-paginations__list-item--hide {
              
      display: none;
              
      width: 0;
          }

          .
      castom-paginations__list-item--one-page::after {
              
      content: '...';
              
      text-align: center;
              
      display: flex;
              
      align-items: center;
              
      padding: 0 5px 0 5px;
          }

          .
      castom-paginations__list-item--end-page::before {
              
      content: '...';
              
      text-align: center;
              
      display: flex;
              
      align-items: end;
              
      padding: 0 5px 0 0;
          }

          @
      media(max-width:768px) {
              .
      castom-paginations__list-button-page {
                  
      font-size: 16px;
              }
          }
      </
      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 === 0 || !this.props.selectorForWrapper)
          return

         if (!
      this.props.range || this.props.range === 0 || 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; i <= this.props.countPages; i++) {
          
      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 (
      i > this.props.range && i !== this.props.countPages) {
           
      this.listItem.classList.add('castom-paginations__list-item--hide')
          }

          if (
      i === 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((item, idx) => {
          if (
      idx !== 0 && 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((item, idx) => {
           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((item, idx) => {
           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.startPage; i > this.props.startPage - this.props.range / 2 - 1; i--) {
          
      this.listItemsAll[i].classList.remove('castom-paginations__list-item--hide')
         }

         for (
      let i = this.props.startPage; i < this.props.startPage + this.props.range / 2 - 1; i++) {
          
      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 === 0 || isNaN(quantityPages)) return
         
          const 
      propsForPagination = {
              
      selectorForWrapper: 'castom-pagination__wrapper',
              
      countPages: quantityPages, // кол-во страниц
              
      range: 5, // сколько страниц отображать
              
      startPage: 1, // номер активной страницы
              
      callback: call, // 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 %>"] {
              
      display: none;
          }
      </
      style>

    6. Клиентский сценарий
      Код:
      
      /* Client scripts module */
      declare const console: any
      declare const insertPagination: any
      declare const clearPagination: any

      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(quantityPages, changePage) // Отрисовать пагинацию
          
      fillTable(1) // Заполнить таблицу
          
      setLastLineNumber() // Установить номер последней строки таблицы
      }


      // callback который передается в инстанс пагинации для листания по страницам
      async function changePage(numPage: number) {
          
      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 = 0; i < Context.data.table.length; i++) {
              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(page: number) {
          if (!
      Context.data.json_table || !Context.data.json_table.length) return

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

          
      for (let i = startRange; i < endRange; i++) {
              if (
      i === 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