concord-consortium/rigse

View on GitHub
rails/react-components/src/library/components/materials-bin/materials-bin.tsx

Summary

Maintainability
B
6 hrs
Test Coverage
import React from "react";

import MBMaterialsCategory from "./materials-category";
import MBCollections from "./collections";
import MBOwnMaterials from "./own-materials";
import MBMaterialsByAuthor from "./materials-by-author";

export default class MaterialsBin extends React.Component<any, any> {
  _isSlugTaken: any;
  constructor (props: any) {
    super(props);

    // it is usually very bad form in React to modify props but we will look the other way this time
    // otherwise we need to clone the array just to add the slug
    const addSlugs = (list: any): any => {
      return (() => {
        const result = [];
        for (const item of Array.from(list) as any[]) {
          if (item.category) {
            item.slug = this.generateSlug(item.category);
            if (item.children) {
              result.push(addSlugs(item.children));
            } else {
              result.push(undefined);
            }
          } else {
            result.push(undefined);
          }
        }
        return result;
      })();
    };
    addSlugs(this.props.materials);

    // selectedSlugs[X] returns a slug that is selected in column X (or falsy value if nothing is selected).
    // E.g. selectedSlugs = ['category-a', 'subcategory-b', 'category-c'] means that:
    // - 'category-a' is selected in the first column,
    // - 'category-b' is selected in the second column,
    // - 'category-c' is selected in the third column.
    this.state = { selectedSlugs: this.selectFirstSlugs() };

    this.checkHash = this.checkHash.bind(this);
    this.handleCellClick = this.handleCellClick.bind(this);
  }

  UNSAFE_componentWillMount () {
    // check the hash at startup and for each change
    jQuery(window).on("hashchange", this.checkHash);
    this.checkHash();
  }

  componentWillUnmount () {
    jQuery(window).off("hashchange", this.checkHash);
  }

  selectFirstSlugs () {
    const selectedSlugs = [];
    let list = this.props.materials;
    while (list?.[0]?.slug) {
      selectedSlugs.push(list[0].slug);
      list = list[0].children;
    }
    return selectedSlugs;
  }

  checkHash () {
    const hash = jQuery.trim(window.location.hash.substr(1));
    const selectedSlugs = hash.length > 0 ? (hash.split("|")) : this.selectFirstSlugs();
    this.setState({ selectedSlugs });
  }

  handleCellClick (column: any, slug: any) {
    // Unselect all the cells that are to the right of modified column.
    const newSlugs = this.state.selectedSlugs.slice(0, column + 1);
    // Select clicked slug
    newSlugs[column] = slug;
    window.location.hash = newSlugs.join("|");
  }

  isSlugSelected (column: any, slug: any) {
    return this.state.selectedSlugs[column] === slug;
  }

  generateSlug (name: any) {
    if (this._isSlugTaken == null) {
      this._isSlugTaken = {};
    }
    let slug = name.toLowerCase().replace(/\W/g, "-");
    while (this._isSlugTaken[slug]) {
      slug += "-";
    }
    this._isSlugTaken[slug] = true;
    return slug;
  }

  // Transforms @props.materials hash into array of arrays representing columns and their rows.
  // Raw form of @props.materials doesn't work well with table view.
  // Also, apply current state (selected cells and visibility).
  _getColumns () {
    const columns: any = [];
    // Adds all elements of `array` to column `columnIdx` and marks them as `visible`.
    // Note that the array is @params.materials at the beginning and then its child elements recursively.
    const fillColumns = (array: any, columnIdx?: any, visible?: any) => {
      if (columnIdx == null) {
        columnIdx = 0;
      }
      if (visible == null) {
        visible = true;
      }
      if (columns[columnIdx] == null) {
        columns[columnIdx] = [];
      }
      array.forEach((cellDef: any) => {
        const selected = this.isSlugSelected(columnIdx, cellDef.slug);
        const rowIdx = columns[columnIdx].length;
        columns[columnIdx].push((() => {
          if (cellDef.category) {
            return (
              <MBMaterialsCategory
                key={rowIdx}
                visible={visible}
                selected={selected}
                column={columnIdx}
                slug={cellDef.slug}
                customClass={cellDef.className}
                loginRequired={cellDef.loginRequired}
                handleClick={this.handleCellClick}
                assignToSpecificClass={this.props.assignToSpecificClass}
              >
                { cellDef.category }
              </MBMaterialsCategory>
            );
          } else if (cellDef.collections) {
            return <MBCollections
              key={rowIdx}
              visible={visible}
              collections={cellDef.collections}
              assignToSpecificClass={this.props.assignToSpecificClass}
            />;
          } else if (cellDef.ownMaterials) {
            return <MBOwnMaterials key={rowIdx} visible={visible} assignToSpecificClass={this.props.assignToSpecificClass} />;
          } else if (cellDef.materialsByAuthor) {
            return <MBMaterialsByAuthor key={rowIdx} visible={visible} assignToSpecificClass={this.props.assignToSpecificClass} />;
          }
        })());
        if (cellDef.children) {
          // Recursively go to children array, add its elements to column + 1
          // and mark them visible only if current cell is selected.
          fillColumns(cellDef.children, columnIdx + 1, selected);
        }
      });
    };

    fillColumns(this.props.materials);
    return columns;
  }

  render () {
    return (
      <div className="materials-bin">
        { this._getColumns().map((column: any, idx: number) => <div key={idx} className="mb-column">{ column }</div>) }
      </div>
    );
  }
}