Source

widgets/dataTables.js

import downloads from '@devoinc/applications-builder/libs/downloads';
import { i18n } from '@devoinc/applications-builder/i18n';
import widgetFactory from '@devoinc/applications-builder/widgetFactory';
import objects from '@devoinc/applications-builder/utils/objects';
import { arrayRenderer, tooltipRenderer } from './helpers/renderers';

/**
 * The table widget displays data as a simple table,
 * distributed in columns and rows.
 *
 * This widget is based on
 * [datatables]{@link https://datatables.net/} library.
 * @category Widgets
 * @module Table
 * @see [base](module-base.html)
 * @see [collapser](module-collapser.html)
 * @see [dataSearch](module-dataSearch.html)
 * @see [download](module-download.html)
 * @see [info](module-info.html)
 * @see [lifeCycle](module-lifeCycle.html)
 * @see [listeners](module-listeners.html)
 * @see [loading](module-loading.html)
 * @see [menu](module-menu.html)
 * @see [screenshot](module-screenshot.html)
 * @see [zoom](module-zoom.html)
 * @tutorial widgets-table
 */
function mixin(self) {
  return {
    /**
     * Set the limit of data to show without any kind of order.
     * @param {number} limit - Limit value.
     * @instance
     */
    setLimit(limit) {
      self.settings.ds.limit = limit;
    },

    /**
     * Set the initial order for the table.
     * @param {Array} order
     * @param {index} order.0 - Index of the column
     * @param {string} order.1 - Order to sorting. <i>desc</i> or <i>asc</i>.
     * @instance
     */
    setOrder(order) {
      self.settings.ds.order = order;
    },

    /**
     * Set the array of labels to show as series title.
     * Override the processed ones.
     * @param {string[]} arr - Array of strings
     * @instance
     */
    setLabels(arr) {
      self.settings.ds.labels = arr;
    },

    /**
     * Set specific options to columns in the table.
     * @param {Object[]} defs - Array of columnDefs objects.
     * @see https://datatables.net/reference/option/columnDefs
     * @instance
     */
    setColumnDefs(defs) {
      self.settings.ds.columnDefs = self.settings.ds.columnDefs || [];
      self.settings.ds.columnDefs = self.settings.ds.columnDefs.concat(defs);
    },

    /**
     * Attach an event handler function to execute when a row is clicked.
     * @param {Function} func - Callback JavaScript function.
     * @instance
     */
    setRowClick(func) {
      self.settings.ds.rowClick = func;
    },

    /**
     * Set a function that is called before every draw of the table.
     * @param {Function} func - Callback JavaScript function.
     * @see https://datatables.net/reference/option/drawCallback
     * @instance
     */
    setDrawCallback(func) {
      self.settings.ds.drawCallback = func;
    },

    /**
     * Set the table control elements to appear on the page and in what order.
     * https://datatables.net/reference/option/dom
     * @param {string} str - Dom value.
     * @see https://datatables.net/reference/option/dom
     * @instance
     */
    setDom(str) {
      self.settings.ds.dom = str;
    },

    /**
     * Set the order of the columns to be displayed in the table based on the
     * values of the query.
     * @param {int[]} order - Index of columns.
     * @instance
     */
    setColumnsOrder(order) {
      self.settings.ds.columnsOrder = order;
    },

    /**
     * Set the language configuration.
     * @param {object} language - Language object.
     * @see https://datatables.net/reference/option/language
     * @instance
     */
    setLanguage(language) {
      self.settings.ds.language = language;
    },

    /**
     * Set the vertical scrolling.
     * @param {string} scrollY - Value in pixels.
     * @see https://datatables.net/reference/option/scrollY
     * @instance
     */
    setScrollY(scrollY) {
      self.settings.ds.scrollY = scrollY;
    },

    /**
     * Set the horizontal scrolling.
     * @param {boolean} bool - Enable or disable.
     * @see https://datatables.net/reference/option/scrollX
     * @instance
     */
    setScrollX(scrollX) {
      self.settings.ds.scrollX = scrollX;
    },

    /**
     * Allow reduces the height of the table when a limited number of
     * rows are shown.
     * @param {boolean} scrollCollapse - Enable or disable.
     * @see https://datatables.net/reference/option/scrollCollapse
     * @instance
     */
    setScrollCollapse(scrollCollapse) {
      self.settings.ds.scrollCollapse = scrollCollapse;
    },

    /**
     * Allow to change the paging display length of the table.
     * @param {boolean} lengthChange - Enable or disable.
     * @see https://datatables.net/reference/option/lengthChange
     * @instance
     */
    setLengthChange(lengthChange) {
      self.settings.ds.lengthChange = lengthChange;
    },

    /**
     * Set the default number of rows per page.
     * @param {number} pageLength - Default page length.
     * @see https://datatables.net/reference/option/pageLength
     * @instance
     */
    setPageLength(pageLength) {
      self.settings.ds.pageLength = pageLength;
    },

    /**
     * Change the options in the page length select list.
     * @param {Array[]} lengthMenu - Array of int values.
     * @see https://datatables.net/reference/option/lengthMenu
     * @default [ 10, 25, 50, 100 ]
     * @instance
     */
    setMenuLength(lengthMenu) {
      self.settings.ds.lengthMenu = lengthMenu;
    },

    /**
     * Callback executed when a row (TR element) is created.
     * @param {Function} func - Callback JavaScript function.
     * @see https://datatables.net/reference/option/createdRow
     * @instance
     */
    setCreatedRow(func) {
      self.settings.ds.createdRow = func;
    },

    /**
     * Use the visual data for exported values.
     * @param {boolean} bool - Enable or disable.
     * @instance
     */
    useExportWithVisual(bool = true) {
      self.settings.useExportWithVisual = bool;
    },

    /**
     * Allow to show the filter.
     * @param {boolean} boo - Enable or disable.
     * @instance
     */
    setFilter(bool = false) {
      objects.set(self, 'settings.ds.filter', bool);
    },

    /**
     * Allow table pagination.
     * @param {boolean} bool - Enable or disable.
     * @see https://datatables.net/reference/option/paging
     * @instance
     */
    setPaging(bool = true) {
      self.settings.ds.paging = bool;
    },

    /**
     * Allow to show information about the table.
     * @param {boolean } bool - Enable or disable.
     * @see https://datatables.net/reference/option/info
     * @instance
     */
    setFooterInfo(bool = true) {
      self.settings.ds.info = bool;
    },

    /**
     * Add custom class to table element.
     * @param {string} classes - Name of clases.
     * @instance
     */
    setCustomClass(classes = []) {
      self.settings.ds.cls = classes;
    },

    /**
     * Set arbitrary renderers to the given columns.
     * @param {Array|number|string} targetColumns - Columns to apply
     * the definitions.
     * @param {Function|Object|string|number} render - Process to apply.
     * @see https://datatables.net/reference/option/columnDefs.targets
     * @see https://datatables.net/reference/option/columns.render
     * @instance
     */
    setColumnRenderers(targetColumns, render) {
      self.settings.ds.columnDefs = self.settings.ds.columnDefs || [];
      self.settings.ds.columnDefs = self.settings.ds.columnDefs.concat({
        render: render,
        targets: targetColumns,
      });
    },

    /**
     * Renders a group of cells with a span and a tooltip.
     * @param {Array|number|string} targetColumns - Columns to apply
     * the definitions.
     * @see https://datatables.net/reference/option/columnDefs.targets
     * @instance
     */
    setToolTipColumns(targetColumns) {
      this.setColumnRenderers(targetColumns, tooltipRenderer);

      self.settings.ds.columnDefs = self.settings.ds.columnDefs || [];
      self.settings.ds.columnDefs.push({
        className: 'flexible-ellipsis-cell',
        targets: targetColumns,
      });
    },

    /**
     * Set which columns should not wrap the text.
     * @param {Array|number|string} targetColumns - Columns to apply
     * the definitions.
     * @see https://datatables.net/reference/option/columnDefs.targets
     * @instance
     */
    setNoWrapColumns(targetColumns) {
      self.settings.ds.columnDefs = self.settings.ds.columnDefs || [];
      self.settings.ds.columnDefs.push({
        className: 'nowrap-cell',
        targets: targetColumns,
      });
    },

    /**
     * Set the columns that should represent an array of values.
     * @param {Array|number|string} targetColumns - Columns to apply
     * the definitions.
     * @see https://datatables.net/reference/option/columnDefs.targets
     * @instance
     */
    setArrayColumns(targetColumns) {
      this.setColumnRenderers(targetColumns, arrayRenderer);
    },

    /**
     * Set the pagination type.
     *
     * - <b>numbers</b>: Page number buttons only.
     * - <b>simple</b>: <i>Previous</i> and <i>Next</i> buttons only.
     * - <b>simple_numbers</b>: <i>Previous</i> and <i>Next</i> buttons,
     * plus page numbers.
     * - <b>full</b>: <i>First</i>, <i>Previous</i>, <i>Next</i> and
     * <i>Last</i> buttons.
     * - <b>full_numbers</b>: <i>First</i>, <i>Previous</i>, <i>Next</i> and
     * <i>Last</i> buttons, plus page numbers.
     * - <b>first_last_numbers</b>: <i>First</i> and <i>Last</i> buttons,
     * plus page numbers.
     * @param {string} pagingType - Pagination type.
     * @see https://datatables.net/reference/option/pagingType
     * @instance
     */
    setPagingType(pagingType = 'simple_numbers') {
      self.settings.ds.pagingType = pagingType;
    },

    // Common
    // -------------------------------------------------------------------------

    /**
     * Download in CSV format
     * @param {string} d - Delimiter (default: ,)
     * @param {string} q - Quotechar (default: ")
     * @ignore
     */
    downloadCSV(d = ',', q = '"') {
      let dt = self.widget;

      // Write headers
      let headers = dt.settings().columns().header();
      let content = headers.map((el) => q + el.innerHTML + q).join(d) + '\n';

      // Write content
      if (self.settings.useExportWithVisual) {
        // Visual content
        let rowIndexes = dt
          .rows(null, {
            search: 'applied',
            order: 'applied',
          })
          .indexes()
          .toArray();
        var selectedCells = dt.cells(rowIndexes, '');
        var cells = selectedCells.render('display').toArray();
        for (let i = 1; i <= cells.length; i++) {
          content += q + cells[i - 1] + q;
          content += i % headers.length === 0 ? '\n' : ',';
        }
      } else {
        // Internal content
        content += self.data
          .map((row) => q + row.join(q + d + q) + q)
          .join('\n');
      }

      // Download CSV
      downloads.downloadCSV(content, self.id);
    },

    // Life Cycle
    // -------------------------------------------------------------------------

    /**
     * Render of the widget
     * @param {arr} orig - Data for the widget
     * @ignore
     */
    render(data) {
      if (!self.el) return; // If not have element not render
      let orig = { ...data };
      let cfg = self.settings;
      if (
        orig &&
        orig.dataMatrix &&
        Array.isArray(orig.dataMatrix) &&
        orig.keys &&
        Array.isArray(orig.keys) &&
        orig.dataMatrix.length > 0
      ) {
        // Initial params
        const params = {
          dom: cfg.ds.dom || 'lfrtip',
        };

        // Data Source
        const safeItem = (text) => $.fn.psDataTable.render.text().display(text);
        var d = orig.dataMatrix.map((e) => e.map((x) => safeItem(x)));
        const dataArr = cfg.ds.limit ? d.slice(0, cfg.ds.limit) : d;
        self.data = d;

        if (cfg.useDataTableSource) {
          const dts = new DataTableSource(dataArr, false);
          params['serverSide'] = true;
          params['deferRender'] = true;
          params['sorting'] = [];
          params['destroy'] = true;
          params['ajax'] = (data, callback, settings) => {
            callback(dts.ajax(data, settings));
          };
        } else {
          params['aaData'] = dataArr;
          // Use deferRender by default.
          params['deferRender'] = true;
        }

        params['aaData'] = dataArr;
        params['deferRender'] = true;

        // Columns
        let labels = orig.keys.map((key) => key.name);
        let columns = [];

        let aux = 0;
        if (cfg.ds.labels)
          for (let h of cfg.ds.labels)
            columns.push({
              sTitle: h,
              data: cfg.ds.columnsOrder ? cfg.ds.columnsOrder[aux++] : aux++,
            });
        else
          for (let h of labels)
            columns.push({
              sTitle: h,
              data: cfg.ds.columnsOrder ? cfg.ds.columnsOrder[aux++] : aux++,
            });

        params['columns'] = columns;

        if (cfg.ds.columnDefs) params['columnDefs'] = cfg.ds.columnDefs;

        // Scroll
        if (cfg.ds.scrollX) params['scrollX'] = cfg.ds.scrollX;
        if (cfg.ds.scrollY) params['scrollY'] = cfg.ds.scrollY;
        if (cfg.ds.scrollCollapse)
          params['scrollCollapse'] = cfg.ds.scrollCollapse;

        // Languages
        let lang = i18n.getLang();
        let locales = i18n.locales[lang] ? i18n.locales[lang].dt : {};
        let userLocales = self.settings.ds.language || {};
        let languageObj = Object.assign({}, locales, userLocales);
        params['language'] = languageObj;
        params['lengthChange'] = cfg.ds.lengthChange || false;
        params['filter'] = cfg.ds.filter || false;

        // Order
        params['order'] = cfg.ds.order ? cfg.ds.order : [];

        // Pagination
        params['pageLength'] = cfg.ds.pageLength ? cfg.ds.pageLength : 10;
        params['paging'] =
          typeof cfg.ds.paging == 'boolean' ? cfg.ds.paging : true;
        params['info'] = typeof cfg.ds.info == 'boolean' ? cfg.ds.info : true;
        params['pagingType'] =
          typeof cfg.ds.pagingType === 'string'
            ? cfg.ds.pagingType
            : 'simple_numbers';
        params['lengthMenu'] = cfg.ds.lengthMenu
          ? cfg.ds.lengthMenu
          : [10, 25, 50, 100];

        // Callbacks
        params['createdRow'] = cfg.ds.createdRow || null;
        params['drawCallback'] = cfg.ds.drawCallback;

        // Create the table base
        let table = document.createElement('table');
        table.classList.add('display', 'hover', 'order-column');
        table.setAttribute('cellspacing', 0);
        table.setAttribute('width', '100%');

        if (typeof cfg.ds.cls !== 'undefined') {
          for (let c of cfg.ds.cls) {
            table.classList.add(c);
          }
        }
        self.graphic.innerHTML = '';
        self.graphic.appendChild(table);

        // Create the widget
        // Always destroy the previous datatable
        if (self.table !== undefined) {
          $(self.table).PsDataTable().destroy();
        }
        self.widget = $(table).PsDataTable(params);

        // Remember the table to destroy it on the next refresh
        self.table = table;

        // Add row click behaviour
        if (cfg.ds.rowClick) {
          $('#' + self.id + ' tbody').on('click', 'tr', cfg.ds.rowClick);
        }
      } else {
        this.debugError({
          msg: 'NO DATA',
          console: {
            method: 'error',
            msg: 'No data arrive to render function',
          },
        });
      }
      return data;
    },
  };
}

export default widgetFactory(mixin);