Commit df2bc970 authored by Sascha Eggenberger's avatar Sascha Eggenberger
Browse files

Issue #3389843: Select all on big lists is unusably slow

parent 5236d875
Loading
Loading
Loading
Loading
Loading

dist/js/tableheader.js

0 → 100644
+20 −0
Original line number Diff line number Diff line
((Drupal, once) => {
  Drupal.behaviors.ginTableHeader = {
    attach: context => {
      Drupal.ginTableHeader.init(context);
    }
  }, Drupal.ginTableHeader = {
    init: function(context) {
      once("ginTableHeader", ".sticky-enabled", context).forEach((el => {
        new IntersectionObserver((_ref => {
          let [e] = _ref;
          context.querySelector(".gin-table-scroll-wrapper") && (e.isIntersecting || e.intersectionRect.top !== Drupal.displace.offsets.top ? context.querySelector(".gin-table-scroll-wrapper").classList.remove("--is-sticky") : context.querySelector(".gin-table-scroll-wrapper").classList.add("--is-sticky"), 
          Drupal.displace(!0));
        }), {
          threshold: 1,
          rootMargin: `-${Drupal.displace.offsets.top}px 0px 0px 0px`
        }).observe(el.querySelector("thead"));
      }));
    }
  };
})(Drupal, once);
 No newline at end of file
+2 −8
Original line number Diff line number Diff line
@@ -16,14 +16,6 @@ libraries:

libraries-override:
  # Gin overrides: replace
  core/drupal.tableselect:
    js:
      misc/tableselect.js: js/overrides/tableselect.js

  core/drupal.tableheader:
    js:
      misc/tableheader.js: js/overrides/tableheader.js

  media_library/view:
    js:
      js/media_library.view.js: js/overrides/media_library.view.js
@@ -52,6 +44,8 @@ libraries-override:
libraries-extend:
  core/drupal.tableselect:
    - gin/tableselect
  core/drupal.tableheader:
    - gin/tableheader
  core/drupal.autocomplete:
    - gin/autocomplete
  core/ckeditor:
+4 −0
Original line number Diff line number Diff line
@@ -185,6 +185,10 @@ tableselect:
    component:
      dist/css/components/tableselect.css: { minified: false }

tableheader:
  js:
    dist/js/tableheader.js: { minified: false }

tabs:
  css:
    component:

js/overrides/tableheader.js

deleted100644 → 0
+0 −85
Original line number Diff line number Diff line
((Drupal, once) => {
  Drupal.behaviors.ginTableHeader = {
    attach: (context) => {
      Drupal.ginTableHeader.init(context);
    },
  };

  Drupal.ginTableHeader = {
    init: function (context) {
      once('ginTableHeader', '.sticky-enabled', context).forEach(el => {
        // Watch sticky table header.
        const stickyOffsetTop = this.stickyPosition();
        const observer = new IntersectionObserver(
          ([e]) => {
            if (context.querySelector('.gin-table-scroll-wrapper')) {
              if (!e.isIntersecting && e.intersectionRect.top === stickyOffsetTop) {
                context.querySelector('.gin-table-scroll-wrapper').classList.add('--is-sticky');
              } else {
                context.querySelector('.gin-table-scroll-wrapper').classList.remove('--is-sticky');
              }
            }
          },
          { threshold: 1.0, rootMargin: `-${stickyOffsetTop}px 0px 0px 0px` }
        );
        observer.observe(el.querySelector('thead'));

        // Create sticky element.
        this.createStickyHeader(el);

        // SelectAll handling.
        this.syncSelectAll();

        // Watch resize event.
        window.onresize = () => {
          Drupal.debounce(this.handleResize(el), 150);
        };
      });
    },
    stickyPosition: () => {
      let offsetTop = 0;
      if (document.body.classList.contains('gin--classic-toolbar')) {
        offsetTop = document.querySelector('#toolbar-bar').clientHeight;
      } else {
        const toolbar = document.querySelector('#gin-toolbar-bar');
        offsetTop = document.querySelector('.region-sticky').clientHeight;
        if (toolbar) {
          offsetTop += toolbar.clientHeight;
        }
      }

      return offsetTop;
    },
    createStickyHeader: function createStickyHeader(table) {
      const header = table.querySelector(':scope > thead');
      const stickyTable = document.createElement('table');
      stickyTable.className = 'sticky-header';
      stickyTable.append(header.cloneNode(true));
      table.insertBefore(stickyTable, header);
      this.handleResize(table);
    },
    syncSelectAll: () => {
      document.querySelectorAll('table.sticky-header th.select-all').forEach(tableHeaderSticky => {
        const table = tableHeaderSticky.closest('table');
        table.querySelectorAll(':scope th.select-all').forEach(tableHeader => {
          tableHeader.addEventListener('click', event => {
            if (event.target.matches('input[type="checkbox"]')) {
              table.nextSibling.querySelectorAll('th.select-all').forEach(siblingTableHeader => {
                siblingTableHeader.childNodes[0].click();
              });
            }
          });
        });
      });
    },
    handleResize: (table) => {
      const header = table.querySelector(':scope > thead');
      header.querySelectorAll('th').forEach((el, i) => {
        table.querySelector(`table.sticky-header > thead th:nth-of-type(${i+1})`).style.width = `${el.offsetWidth}px`;
        table.querySelector(`table.sticky-header`).style.width = `${el.parentNode.offsetWidth}px`;
      });
    },

  };

})(Drupal, once);

js/overrides/tableselect.js

deleted100644 → 0
+0 −128
Original line number Diff line number Diff line
((Drupal, once) => {
  Drupal.behaviors.tableSelect = {
    attach: (context) => {
      once('tableSelect', 'th.select-all', context).forEach((el) => {
        if (el.closest('table')) {
          Drupal.tableSelect(el.closest('table'));
        }
      });
    },
  };

  Drupal.tableSelect = (table) => {
    if (table.querySelector('td input[type="checkbox"]') === null) {
      return;
    }

    let checkboxes = 0;
    let lastChecked = 0;
    const strings = {
      selectAll: Drupal.t('Select all rows in this table'),
      selectNone: Drupal.t('Deselect all rows in this table')
    };
    const updateSelectAll = (state) => {
      table
        .querySelectorAll('th.select-all input[type="checkbox"]')
        .forEach(checkbox => {
          const stateChanged = checkbox.checked !== state;
          checkbox.setAttribute(
            'title',
            state ? strings.selectNone : strings.selectAll
          );

          if (stateChanged) {
            checkbox.checked = state;
            checkbox.dispatchEvent(new Event('change'));
          }
        });
    };

    const setClass = 'is-sticky';
    const stickyHeader = table
      .closest('form')
      .querySelector('[data-drupal-selector*="edit-header"]');

    const updateSticky = (state) => {
      if (stickyHeader) {
        if (state === true) {
          stickyHeader.classList.add(setClass);
        }
        else {
          stickyHeader.classList.remove(setClass);
        }
      }
    };

    const checkedCheckboxes = (checkboxes) => {
      const checkedCheckboxes = Array.from(checkboxes).filter(checkbox => checkbox.matches(':checked'));
      updateSelectAll(checkboxes.length === checkedCheckboxes.length);
      updateSticky(!!checkedCheckboxes.length);
    };

    table.querySelectorAll('th.select-all').forEach(el => {
      el.innerHTML = Drupal.theme('checkbox') + el.innerHTML;
      el.querySelector('.form-checkbox').setAttribute('title', strings.selectAll);
      el.addEventListener('click', event => {
        if (event.target.matches('input[type="checkbox"]')) {
          checkboxes.forEach(checkbox => {
            const stateChanged = checkbox.checked !== event.target.checked;

            if (stateChanged) {
              checkbox.checked = event.target.checked;
              checkbox.dispatchEvent(new Event('change'));
            }

            checkbox.closest('tr').classList.toggle('selected', checkbox.checked);
          });

          updateSelectAll(event.target.checked);
          updateSticky(event.target.checked);
        }
      });
    });

    checkboxes = table.querySelectorAll('td input[type="checkbox"]:enabled');
    checkboxes.forEach(el => {
      el.addEventListener('click', e => {
        e.target
          .closest('tr')
          .classList.toggle('selected', this.checked);

        if (e.shiftKey && lastChecked && lastChecked !== e.target) {
          Drupal.tableSelectRange(
            e.target.closest('tr'),
            lastChecked.closest('tr'),
            e.target.checked
          );
        }

        checkedCheckboxes(checkboxes);
        lastChecked = e.target;
      });
    });

    checkedCheckboxes(checkboxes);
  };

  Drupal.tableSelectRange = function (from, to, state) {
    const mode = from.rowIndex > to.rowIndex ? 'previousSibling' : 'nextSibling';

    for (let i = from[mode]; i; i = i[mode]) {
      if (i.nodeType !== 1) {
        continue;
      }

      i.classList.toggle('selected', state);
      i.querySelector('input[type="checkbox"]').checked = state;

      if (to.nodeType) {
        if (i === to) {
          break;
        }
      } else if ([i].filter(y => y === to).length) {
        break;
      }
    }
  };

})(Drupal, once);
Loading