seed-ui/seedsontable.js

View on GitHub
src/seedsondata.ts

Summary

Maintainability
A
2 hrs
Test Coverage
import * as Handsontable from "handsontable";

export type Validator = (value: any, callback: (condition: boolean) => void) => void;

export interface _ColumnProperties extends CellProperties {
  data?: string | number;
  title?: string;
  editor?: string | Handsontable.EditorConstructor;
  selectOptions?: any[];
  width?: number;
}

export interface CellProperties {
  renderer?: Handsontable.CellRenderer;
  type?: string;
  readOnly?: boolean;
  language?: string;
  format?: string;
  validator?: Validator;
  allowInvalid?: boolean;
  className?: string;
}

export interface ColumnProperties extends _ColumnProperties {
  data: string;
  dataLabel?: string;
  noSeed?: boolean;
}

export interface DataRow {
  id: number;
  [name: string]: string | number | undefined;
}

export interface ContentDataOptions {
  denyNoSeed?: boolean;
}

export interface DataComments {
  [row: string]: {
    [prop: string]: string | undefined;
  }
}

export class SeedsonData {
  private readonly _columns: ColumnProperties[];
  private readonly _columnNames: string[];
  private readonly _columnLabels: string[];
  private readonly _data: DataRow[];
  private readonly _comments: DataComments;

  constructor(columns: ColumnProperties[], sourceData: DataRow[] = [], comments: DataComments = {}) {
    this._columns = columns;
    this._columnNames = SeedsonData.namesFromColumns(columns);
    this._columnLabels = this.columns.map((column) => `${column.dataLabel || ""}<br>[${column.data}]`);
    this._data = sourceData.map((row) => Object.assign({}, row));
    this._comments = comments;
  }

  static fromHash(columns: ColumnProperties[], sourceData: {[name: string]: DataRow}, comments?: DataComments) {
    return new SeedsonData(columns, SeedsonData.hashToNative(sourceData), comments);
  }

  static fromArray(columns: ColumnProperties[], sourceData: Array<Array<any>>, comments?: DataComments) {
    return new SeedsonData(columns, SeedsonData.arrayToNative(columns, sourceData), comments);
  }

  static hashToNative(data: {[name: string]: DataRow}) {
    return Object.keys(data).map((key) => data[key]).sort((a, b) => (a.id || 0) - (b.id || 0));
  }

  static nativeToHash(data: DataRow[]) {
    return data.reduce((hash, row) => {
      if (row.id != null) hash[`data${row.id}`] = row;
      return hash;
    }, {} as {[name: string]: DataRow});
  }

  static arrayToNative(columns: ColumnProperties[], data: Array<Array<any>>) {
    const columnNames = SeedsonData.namesFromColumns(columns);
    return data.map((row) =>
      columnNames.reduce((hash, name, index) => {
        hash[name] = row[index];
        return hash;
      }, {} as DataRow)
    );
  }

  static nativeToArray(columns: ColumnProperties[], data: DataRow[]) {
    const columnNames = SeedsonData.namesFromColumns(columns);
    return data.map((row) => columnNames.map((name) => row[name]));
  }

  static namesFromColumns(columns: ColumnProperties[]) {
    return columns.map((column) => column.data);
  }

  get columns() { return this._columns; }

  get columnNames() { return this._columnNames; }

  get columnLabels() { return this._columnLabels; }

  get data() { return this._data; }

  get comments() { return this._comments; }

  contentData(options: ContentDataOptions = {}) {
    const {denyNoSeed} = options;
    let columns = this.columns;
    if (denyNoSeed) {
      columns = columns.filter((column) => !column.noSeed);
    }
    if (columns.length === this.columns.length) {
      return this.data.filter(row => row.id).map((row) => Object.assign({}, row));
    } else {
      const columnNames = SeedsonData.namesFromColumns(columns);
      return this.data.filter(row => row.id).map((row) =>
        columnNames.reduce((newrow, name) => {
          newrow[name] = row[name];
          return newrow;
        }, {} as DataRow)
      );
    }
  }

  contentDataToHash(options?: ContentDataOptions) {
    return SeedsonData.nativeToHash(this.contentData(options));
  }

  contentDataToArray(options?: ContentDataOptions) {
    return SeedsonData.nativeToArray(this.columns, this.contentData(options));
  }

  setDataAtRowProp(row: Array<[number, string, string]>): void;
  setDataAtRowProp(row: number, prop: string, value: string): void;
  setDataAtRowProp(row: number | Array<[number, string, string]>, prop?: string, value?: string) {
    const inputs = SeedsonData.setDataInputToArray(row as number, prop as string, value as string);
    inputs.forEach(([row, prop, value]) => {
      if (!this.data[row]) this.data[row] = {} as DataRow;
      this.data[row][prop] = value;
    });
  }

  static setDataInputToArray(row: Array<[number, string, string]>): [number, string, string][];
  static setDataInputToArray(row: number, propOrCol: string, value: string): [number, string, string][];
  static setDataInputToArray(row: number | Array<[number, string, string]>, propOrCol?: string, value?: string) {
    if (typeof row === 'object') { // is it an array of changes
      return row;
    } else {
      return [
        [row, propOrCol, value] as [number, string, string],
      ];
    }
  }

  get allComments() {
    return Object.keys(this.comments).map((row) =>
      Object.keys(this.comments[row]).map((prop) =>
        ({row, col: this.columnNames.indexOf(prop), comment: this.comments[row][prop]})
      )
    ).reduce((all, part) => all.concat(part), []);
  }

  saveCommentAtRowProp(row: number, prop: string, comment: string) {
    if (!this.comments[row]) this.comments[row] = {};
    this.comments[row][prop] = comment;
  }

  removeCommentAtRowProp(row: number, prop: string) {
    if (this.comments[row]) delete this.comments[row][prop];
    if (Object.keys(this.comments[row]).length === 0) delete this.comments[row];
  }
}