Source: blueprintjs.js

'use strict'

import {observers} from "./observers";
import {widgets} from "./widgets";
import React from 'react';
import ReactDOM from 'react-dom';
import {
  Alignment,
  Button,
  Card,
  Checkbox,
  Drawer,
  FileInput,
  Icon,
  InputGroup,
  Intent,
  Menu,
  MenuItem,
  NumericInput,
  Position,
  RadioGroup,
  RangeSlider,
  Slider,
  Spinner,
  SpinnerSize,
  Switch,
  Tab,
  Tabs,
  Toast,
  Toaster
} from '@blueprintjs/core';
import {Cell, Column, ColumnHeaderCell, Table2, TableLoadingOption} from '@blueprintjs/table';
import {MultiSelect2, Select2, Suggest2} from '@blueprintjs/select';
import {TimePrecision} from '@blueprintjs/datetime';
import {DateInput2, DateRangeInput2} from '@blueprintjs/datetime2';
import {add, format, parse, sub} from "date-fns";
import {strings} from "./strings";

/**
 * @module blueprintjs
 */
export const blueprintjs = {};

/**
 * Base class that deals with injecting the common styles and scripts.
 *
 * @memberOf module:blueprintjs
 * @type {blueprintjs.Blueprintjs}
 */
blueprintjs.Blueprintjs = class extends widgets.Widget {

  /**
   * @param {Element} container the parent element.
   * @constructor
   */
  constructor(container) {
    super(container);
  }

  /**
   * Populate a DOM element with Blueprintjs components.
   *
   * @param template the DOM element.
   * @param objs the components.
   */
  static populate(template, objs) {
    objs.forEach(obj => blueprintjs.Blueprintjs.component(template, obj));
  }

  /**
   * Create a Blueprintjs component from a JSON object.
   *
   * @param template the DOM element where the component will be added.
   * @param obj the component properties.
   */
  static component(template, obj) {

    // {
    //    type: '<string>',
    //    container: '<string>',
    //    el: null,
    //    ...
    // }
    const container = template.querySelector(`#${obj.container}`);
    if (!container) {
      return obj;
    }
    switch (obj.type) {
      case 'Table': {
        obj.el = new blueprintjs.MinimalTable(container);
        break;
      }
      case 'Select': {
        const itemToText = obj.item_to_text;
        const itemToLabel = obj.item_to_label;
        const itemPredicate = obj.item_predicate;
        const itemCreate = obj.item_create;
        obj.el = new blueprintjs.MinimalSelect(container, itemToText, itemToLabel, itemPredicate, itemCreate);
        break;
      }
      case 'Slider': {
        const min = obj.min;
        const max = obj.max;
        const increment = obj.increment;
        const displayIncrement = obj.display_increment;
        obj.el = new blueprintjs.MinimalSlider(container, min, max, increment, displayIncrement);
        break;
      }
      case 'RangeSlider': {
        const min = obj.min;
        const max = obj.max;
        const increment = obj.increment;
        const displayIncrement = obj.display_increment;
        const defaultMinValue = obj.default_min_value;
        const defaultMaxValue = obj.default_max_value;
        obj.el = new blueprintjs.MinimalRangeSlider(container, min, max, increment, displayIncrement, defaultMinValue,
          defaultMaxValue);
        break;
      }
      case 'Drawer': {
        const width = obj.width;
        obj.el = new blueprintjs.MinimalDrawer(container, width);
        break;
      }
      case 'Tabs': {
        obj.el = new blueprintjs.MinimalTabs(container);
        break;
      }
      case 'Spinner': {
        const size = obj.size;
        obj.el = new blueprintjs.MinimalSpinner(container, size);
        break;
      }
      case 'Switch': {
        const checked = obj.checked;
        const label = obj.label;
        const labelPosition = obj.label_position;
        const labelChecked = obj.label_checked;
        const labelUnchecked = obj.label_unchecked;
        obj.el = new blueprintjs.MinimalSwitch(container, checked, label, labelPosition, labelChecked, labelUnchecked);
        break;
      }
      case 'Toaster': {
        obj.el = new blueprintjs.MinimalToaster(container);
        break;
      }
      case 'Card': {
        const body = obj.body;
        obj.el = new blueprintjs.MinimalCard(container, body);
        break;
      }
      case 'Icon': {
        const icon = obj.icon;
        const intent = obj.intent;
        obj.el = new blueprintjs.MinimalIcon(container, icon, intent);
        break;
      }
      case 'Checkbox': {
        const checked = obj.checked;
        const label = obj.label;
        const labelPosition = obj.label_position;
        obj.el = new blueprintjs.MinimalCheckbox(container, checked, label, labelPosition);
        break;
      }
      case 'Date': {
        const format = obj.format;
        const minDate = obj.min_date;
        const maxDate = obj.max_date;
        obj.el = new blueprintjs.MinimalDate(container, format, minDate, maxDate);
        break;
      }
      case 'Datetime': {
        const format = obj.format;
        const minDate = obj.min_date;
        const maxDate = obj.max_date;
        const timePrecision = obj.default_precision;
        const defaultTimezone = obj.default_timezone;
        obj.el = new blueprintjs.MinimalDatetime(container, format, minDate, maxDate, timePrecision, defaultTimezone);
        break;
      }
      case 'DateRange': {
        const format = obj.format;
        const minDate = obj.min_date;
        const maxDate = obj.max_date;
        obj.el = new blueprintjs.MinimalDateRange(container, format, minDate, maxDate);
        break;
      }
      case 'MultiSelect': {
        const itemToText = obj.item_to_text;
        const itemToLabel = obj.item_to_label;
        const itemToTag = obj.item_to_tag;
        const itemPredicate = obj.item_predicate;
        const itemCreate = obj.item_create;
        obj.el = new blueprintjs.MinimalMultiSelect(container, itemToText, itemToLabel, itemToTag, itemPredicate,
          itemCreate);
        break;
      }
      case 'Suggest': {
        const itemToText = obj.item_to_text;
        const itemToLabel = obj.item_to_label;
        const itemPredicate = obj.item_predicate;
        obj.el = new blueprintjs.MinimalSuggest(container, itemToText, itemToLabel, itemPredicate);
        break;
      }
      case 'FileInput': {
        const multiple = obj.multiple;
        obj.el = new blueprintjs.MinimalFileInput(container, multiple);
        break;
      }
      case 'RadioGroup': {
        const label = obj.label;
        const inline = obj.inline;
        obj.el = new blueprintjs.MinimalRadioGroup(container, label, inline);
        break;
      }
      case 'TextInput': {
        const defaultValue = obj.default_value;
        const icon = obj.icon;
        const intent = obj.intent;
        obj.el = new blueprintjs.MinimalTextInput(container, defaultValue, icon, intent);
        break;
      }
      case 'NumericInput': {
        const min = obj.min;
        const max = obj.max;
        const increment = obj.increment;
        const defaultValue = obj.default_value;
        const icon = obj.icon;
        const intent = obj.intent;
        obj.el = new blueprintjs.MinimalNumericInput(container, min, max, increment, defaultValue, icon, intent);
        break;
      }
      case 'Button': {
        const label = obj.label;
        const labelPosition = obj.label_position;
        const leftIcon = obj.left_icon;
        const rightIcon = obj.right_icon;
        const intent = obj.intent;
        obj.el = new blueprintjs.MinimalButton(container, label, labelPosition, leftIcon, rightIcon, intent);
        break;
      }
      default:
        obj.el = null;
        break;
    }
    if (obj.el) {
      for (let key in obj) {
        if (key === 'type' || key === 'container' || key === 'el') {
          continue;
        }
        const prop = strings.snakeCaseToCamelCase(key);
        const desc = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(obj.el), prop);
        // console.log(prop, desc);
        if (desc) {
          if (desc.set) { // setter
            obj.el[prop] = obj[key];
          } else if (desc.writable) { // function
            obj.el[prop](obj[key]);
          }
        }
      }
    }
    return obj;
  }

  /**
   * In order to avoid a memory leak, properly remove the component from the DOM.
   *
   * @name destroy
   * @function
   * @protected
   */
  destroy() {

    if (this.widgets_) {

      // Remove registered widgets
      for (let i = 0; i < this.widgets_.length; i++) {
        this.widgets_[i].destroy();
      }
      this.widgets_ = [];
    }

    ReactDOM.unmountComponentAtNode(this.container);
  }

  /**
   * Renders the component.
   *
   * @name render
   * @function
   * @protected
   */
  render() {
    const element = this._newElement();
    if (element) {
      ReactDOM.render(element, this.container);
    }
  }

  /**
   * Initializes the component.
   *
   * @return {ReactElement|null}
   * @name _newElement
   * @function
   * @protected
   */
  _newElement() {
    return null;
  }
}

/**
 * A skeleton to ease the creation of a minimal Blueprintjs table component.
 *
 * @memberOf module:blueprintjs
 * @extends {blueprintjs.Blueprintjs}
 * @type {blueprintjs.MinimalTable}
 */
blueprintjs.MinimalTable = class extends blueprintjs.Blueprintjs {

  /**
   * @param {Element} container the parent element.
   * @param {function(number, number, *): ReactElement} cellRenderer a function in charge of rendering a single cell (optional).
   * @constructor
   */
  constructor(container, cellRenderer) {
    super(container);
    this.cellRenderer_ = cellRenderer;
    this.observers_ = new observers.Subject();
    this.columns_ = [];
    this.columnWidths_ = [];
    this.columnTypes_ = [];
    this.rows_ = [];
    this.loadingOptions_ = [];
    this.render();
  }

  get columns() {
    return this.columns_;
  }

  set columns(value) {
    this.columns_ = value;
    this.render();
  }

  get columnTypes() {
    return this.columnTypes_;
  }

  set columnTypes(values) {
    this.columnTypes_ = values;
    this.render();
  }

  get columnWidths() {
    return this.columnWidths_;
  }

  set columnWidths(values) {
    this.columnWidths_ = values;
    this.render();
  }

  get rows() {
    return this.rows_;
  }

  set rows(values) {
    this.rows_ = values;
    this.render();
  }

  get loadingOptions() {
    return this.loadingOptions_;
  }

  set loadingOptions(values) {
    this.loadingOptions_ = values;
    this.render();
  }

  /**
   * Listen to the `sort` event.
   *
   * @param {function(string, string): void} callback the callback to call when the event is triggered.
   * @name onSortColumn
   * @function
   * @public
   */
  onSortColumn(callback) {
    this.observers_.register('sort', (column, order) => {
      // console.log('Sort ' + order + ' is ' + column);
      if (callback) {
        callback(column, order);
      }
    });
  }

  /**
   * Listen to the `fetch-next-rows` event.
   *
   * @param {function(number): void} callback the callback to call when the event is triggered.
   * @name onFetchNextRows
   * @function
   * @public
   */
  onFetchNextRows(callback) {
    this.observers_.register('fetch-next-rows', (nextRow) => {
      // console.log('Next row is ' + nextRow);
      if (callback) {
        callback(nextRow);
      }
    });
  }

  /**
   * Listen to the `selection-change` event.
   *
   * @param {function(Array<Object>): void} callback the callback to call when the event is triggered.
   * @name onSelectionChange
   * @function
   * @public
   */
  onSelectionChange(callback) {
    this.observers_.register('selection-change', (regions) => {
      // console.log('Selected regions are ', regions);
      if (callback) {

        const cells = [];

        for (let i = 0; i < regions.length; i++) {

          if (!regions[i].hasOwnProperty('rows')) {
            continue; // ignore the region if a whole column has been selected
          }
          if (!regions[i].hasOwnProperty('cols')) {
            continue; // ignore the region if a whole row has been selected
          }

          const rows = regions[i].rows;
          const columns = regions[i].cols;

          for (let j = rows[0]; j <= rows[1]; j++) {
            for (let k = columns[0]; k <= columns[1]; k++) {
              cells.push({row_idx: j, col_idx: k, value: this.rows[j][k]});
            }
          }
        }
        callback(cells);
      }
    });
  }

  _newCell(self, rowIdx, colIdx) {
    return self.cellRenderer_ ? self.cellRenderer_(rowIdx, colIdx, self.rows[rowIdx][colIdx]) : React.createElement(
      Cell, {
        rowIndex: rowIdx, columnIndex: colIdx, style: {
          'text-align': self.columnTypes[colIdx] === 'number' ? 'right' : 'left'
        }, children: React.createElement('div', {}, self.rows[rowIdx][colIdx]),
      });
  }

  _newColumnHeader(self, column) {
    return React.createElement(ColumnHeaderCell, {
      name: column, menuRenderer: () => {

        // Menu item for sorting the column in ascending order
        const menuItemSortAsc = React.createElement(MenuItem, {
          icon: 'sort-asc', text: 'Sort Asc', onClick: () => self.observers_.notify('sort', column, 'ASC'),
        });

        // Menu item for sorting the column in descending order
        const menuItemSortDesc = React.createElement(MenuItem, {
          icon: 'sort-desc', text: 'Sort Desc', onClick: () => self.observers_.notify('sort', column, 'DESC'),
        });

        return React.createElement(Menu, {
          children: [menuItemSortAsc, menuItemSortDesc]
        });
      }
    });
  }

  _newColumn(self, column) {
    return React.createElement(Column, {
      name: column,
      cellRenderer: (rowIdx, colIdx) => self._newCell(self, rowIdx, colIdx),
      columnHeaderCellRenderer: () => self._newColumnHeader(self, column),
    });
  }

  _newElement() {
    return React.createElement(Table2, {
      numRows: this.rows.length,
      children: this.columns.map(column => this._newColumn(this, column)),
      enableColumnReordering: true,
      loadingOptions: this.loadingOptions,
      columnWidths: this.columnWidths.length <= 0 ? null : this.columnWidths,
      onSelection: (regions) => {
        this.observers_.notify('selection-change', regions);
      },
      onVisibleCellsChange: (rowIndex, columnIndex) => {
        if (rowIndex.rowIndexEnd + 1 >= this.rows.length) {
          this.observers_.notify('fetch-next-rows', this.rows.length);
        }
      },
      onColumnsReordered: (oldIndex, newIndex, length) => {

        this.loadingOptions = [TableLoadingOption.CELLS];

        // First, reorder the rows header
        const oldColumnsOrder = this.columns;
        const newColumnsOrder = [];

        const oldColumnTypes = this.columnTypes;
        const newColumnTypes = [];

        for (let i = 0; i < oldColumnsOrder.length; i++) {
          if (!(oldIndex <= i && i < oldIndex + length)) {
            newColumnsOrder.push(oldColumnsOrder[i]);
            newColumnTypes.push(oldColumnTypes[i]);
          }
        }
        for (let k = oldIndex; k < oldIndex + length; k++) {
          newColumnsOrder.splice(newIndex + k - oldIndex, 0, oldColumnsOrder[k]);
          newColumnTypes.splice(newIndex + k - oldIndex, 0, oldColumnTypes[k]);
        }

        // console.log('Previous column order was [' + oldColumnsOrder.join(', ') + ']');
        // console.log('New column order is [' + newColumnsOrder.join(', ') + ']');

        // console.log('Previous column types were [' + oldColumnTypes.join(', ') + ']');
        // console.log('New column types is [' + newColumnTypes.join(', ') + ']');

        // Then, reorder the rows data
        const oldColumnsIndex = {};
        const newColumnsIndex = {};

        for (let i = 0; i < oldColumnsOrder.length; i++) {
          oldColumnsIndex[oldColumnsOrder[i]] = i;
        }
        for (let i = 0; i < newColumnsOrder.length; i++) {
          newColumnsIndex[i] = newColumnsOrder[i];
        }

        const oldRows = this.rows;
        const newRows = [];

        for (let i = 0; i < oldRows.length; i++) {

          const newRow = [];

          for (let j = 0; j < oldRows[i].length; j++) {
            newRow.push(oldRows[i][oldColumnsIndex[newColumnsIndex[j]]]);
          }
          newRows.push(newRow);
        }

        // Next, redraw the table
        this.columns = newColumnsOrder;
        this.columnTypes = newColumnTypes;
        this.rows = newRows;
        this.loadingOptions = [];
      },
    });
  }
}

/**
 * A skeleton to ease the creation of a minimal Blueprintjs select element.
 *
 * @memberOf module:blueprintjs
 * @extends {blueprintjs.Blueprintjs}
 * @type {blueprintjs.MinimalSelect}
 */
blueprintjs.MinimalSelect = class extends blueprintjs.Blueprintjs {

  /**
   * @param {Element} container the parent element.
   * @param {function(*): string} itemToText a function that maps an item to the text to be displayed (optional).
   * @param {function(*): string} itemToLabel a function that maps an item to the label to be displayed (optional).
   * @param {function(string, *): boolean} itemPredicate a function that filters the internal list of items when user enters something in the input (optional).
   * @param {function(string): *} itemCreate a function that creates an item from a string (optional).
   * @constructor
   */
  constructor(container, itemToText, itemToLabel, itemPredicate, itemCreate) {
    super(container);
    this.itemToText_ = itemToText;
    this.itemToLabel_ = itemToLabel;
    this.itemPredicate_ = (query, item) => {
      if (itemPredicate) {
        return itemPredicate(query, item);
      }
      if (query && query !== '') {
        const txt = this.itemToText_ ? this.itemToText_(item) : item;
        return txt.trim().toLowerCase().indexOf(query.trim().toLowerCase()) >= 0;
      }
      return true;
    };
    this.itemCreate_ = itemCreate;
    this.observers_ = new observers.Subject();
    this.selectedItem_ = null;
    this.fillContainer_ = true;
    this.disabled_ = false;
    this.filterable_ = true;
    this.items_ = [];
    this.defaultText_ = 'Sélectionnez un élément...';
    this.noResults_ = 'Il n\'y a aucun résultat pour cette recherche.';
    this.render();
  }

  get fillContainer() {
    return this.fillContainer_;
  }

  set fillContainer(value) {
    this.fillContainer_ = value;
    this.render();
  }

  get disabled() {
    return this.disabled_;
  }

  set disabled(value) {
    this.disabled_ = value;
    this.render();
  }

  get filterable() {
    return this.filterable_;
  }

  set filterable(value) {
    this.filterable_ = value;
    this.render();
  }

  get items() {
    return this.items_;
  }

  set items(values) {
    this.items_ = values;
    this.render();
  }

  get selectedItem() {
    return this.selectedItem_;
  }

  set selectedItem(value) {
    this.selectedItem_ = value;
    this.render();
  }

  get defaultText() {
    return this.defaultText_;
  }

  set defaultText(value) {
    this.defaultText_ = value;
    this.render();
  }

  get noResults() {
    return this.noResults_;
  }

  set noResults(value) {
    this.noResults_ = value;
    this.render();
  }

  /**
   * Listen to the `selection-change` event.
   *
   * @param {function(*): void} callback the callback to call when the event is triggered.
   * @name onSelectionChange
   * @function
   * @public
   */
  onSelectionChange(callback) {
    this.observers_.register('selection-change', (item) => {
      // console.log('Selected item is ', item);
      if (callback) {
        callback(item);
      }
    });
  }

  /**
   * Listen to the `filter-change` event.
   *
   * @param {function(*): void} callback the callback to call when the event is triggered.
   * @name onFilterChange
   * @function
   * @public
   */
  onFilterChange(callback) {
    this.observers_.register('filter-change', (filter) => {
      // console.log('Filter is ', filter);
      if (callback) {
        callback(filter);
      }
    });
  }

  _newButton() {
    return React.createElement(Button, {
      text: this.selectedItem ? this.itemToText_ ? this.itemToText_(this.selectedItem) : this.selectedItem
        : this.defaultText,
      alignText: 'left',
      rightIcon: 'double-caret-vertical',
      fill: this.fillContainer,
      disabled: this.disabled,
    });
  }

  _newElement() {
    return React.createElement(Select2, {
      fill: this.fillContainer,
      disabled: this.disabled,
      children: [this._newButton()],
      items: this.items,
      filterable: this.filterable,
      itemPredicate: this.itemPredicate_,
      onItemSelect: (item) => {
        // If the user selects twice the same item, removes the selection
        const selection = item === this.selectedItem ? null : item;
        this.selectedItem_ = selection;
        this.render();
        this.observers_.notify('selection-change', selection);
      },
      onQueryChange: (query) => {
        this.observers_.notify('filter-change', query);
      },
      itemRenderer: (item, props) => {
        if (!props.modifiers.matchesPredicate) {
          return null;
        }
        return React.createElement(MenuItem, {
          key: props.index,
          selected: props.modifiers.active,
          text: this.itemToText_ ? this.itemToText_(item) : item,
          label: this.itemToLabel_ ? this.itemToLabel_(item) : '',
          onFocus: props.handleFocus,
          onClick: props.handleClick,
        });
      },
      noResults: React.createElement(MenuItem, {
        text: this.noResults, disabled: true,
      }),
      popoverProps: {
        matchTargetWidth: true,
      },
      createNewItemFromQuery: this.itemCreate_,
      createNewItemRenderer: (query, active, handleClick) => {
        return React.createElement(MenuItem, {
          icon: 'add', selected: active, text: query, onClick: handleClick,
        });
      },
    });
  }
}

/**
 * A skeleton to ease the creation of a minimal Blueprintjs slider element.
 *
 * @memberOf module:blueprintjs
 * @extends {blueprintjs.Blueprintjs}
 * @type {blueprintjs.MinimalSlider}
 */
blueprintjs.MinimalSlider = class extends blueprintjs.Blueprintjs {

  /**
   * @param {Element} container the parent element.
   * @param {number} min the minimum value.
   * @param {number} max the maximum value.
   * @param {number} increment the internal increment.
   * @param {number} displayIncrement the display increment.
   * @constructor
   */
  constructor(container, min, max, increment, displayIncrement) {
    super(container);
    this.min_ = min;
    this.max_ = max;
    this.increment_ = increment;
    this.displayIncrement_ = displayIncrement;
    this.value_ = min;
    this.observers_ = new observers.Subject();
    this.disabled_ = false;
    this.render();
  }

  get disabled() {
    return this.disabled_;
  }

  set disabled(value) {
    this.disabled_ = value;
    this.render();
  }

  get value() {
    return this.value_;
  }

  set value(value) {
    this.value_ = value;
    this.render();
  }

  /**
   * Listen to the `selection-change` event.
   *
   * @param {function(number): void} callback the callback to call when the event is triggered.
   * @name onSelectionChange
   * @function
   * @public
   */
  onSelectionChange(callback) {
    this.observers_.register('selection-change', (value) => {
      // console.log('Selected value is ', item);
      if (callback) {
        callback(value);
      }
    });
  }

  _newElement() {
    return React.createElement(Slider, {
      min: this.min_,
      max: this.max_,
      stepSize: this.increment_,
      labelStepSize: this.displayIncrement_,
      value: this.value,
      disabled: this.disabled,
      onChange: (value) => {
        this.value = value;
        this.observers_.notify('selection-change', value);
      },
    });
  }
}

/**
 * A skeleton to ease the creation of a minimal Blueprintjs range slider element.
 *
 * @memberOf module:blueprintjs
 * @extends {blueprintjs.Blueprintjs}
 * @type {blueprintjs.MinimalRangeSlider}
 */
blueprintjs.MinimalRangeSlider = class extends blueprintjs.Blueprintjs {

  /**
   * @param {Element} container the parent element.
   * @param {number} min the minimum value.
   * @param {number} max the maximum value.
   * @param {number} increment the internal increment.
   * @param {number} displayIncrement the display increment.
   * @param {number} defaultMinValue the minimum value selected when the component is rendered the first time.
   * @param {number} defaultMaxValue the maximum value selected when the component is rendered the first time.
   * @constructor
   */
  constructor(container, min, max, increment, displayIncrement, defaultMinValue, defaultMaxValue) {
    super(container);
    this.min_ = min;
    this.max_ = max;
    this.increment_ = increment;
    this.displayIncrement_ = displayIncrement;
    this.minValue_ = defaultMinValue;
    this.maxValue_ = defaultMaxValue;
    this.observers_ = new observers.Subject();
    this.disabled_ = false;
    this.render();
  }

  get disabled() {
    return this.disabled_;
  }

  set disabled(value) {
    this.disabled_ = value;
    this.render();
  }

  get minValue() {
    return this.minValue_;
  }

  set minValue(value) {
    this.minValue_ = value;
    this.render();
  }

  get maxValue() {
    return this.maxValue_;
  }

  set maxValue(value) {
    this.maxValue_ = value;
    this.render();
  }

  /**
   * Listen to the `selection-change` event.
   *
   * @param {function(number, number): void} callback the callback to call when the event is triggered.
   * @name onSelectionChange
   * @function
   * @public
   */
  onSelectionChange(callback) {
    this.observers_.register('selection-change', (value) => {
      // console.log('Selected value is ', value);
      if (callback) {
        callback(value[0], value[1]);
      }
    });
  }

  _newElement() {
    return React.createElement(RangeSlider, {
      min: this.min_,
      max: this.max_,
      stepSize: this.increment_,
      labelStepSize: this.displayIncrement_,
      value: [this.minValue, this.maxValue],
      disabled: this.disabled,
      onChange: (value) => {
        this.minValue = value[0];
        this.maxValue = value[1];
        this.observers_.notify('selection-change', value);
      },
    });
  }
}

/**
 * A skeleton to ease the creation of a minimal Blueprintjs drawer element.
 *
 * @memberOf module:blueprintjs
 * @extends {blueprintjs.Blueprintjs}
 * @type {blueprintjs.MinimalDrawer}
 */
blueprintjs.MinimalDrawer = class extends blueprintjs.Blueprintjs {

  /**
   * @param {Element} container the parent element.
   * @param {string} width the drawer width in pixels or percents (optional).
   * @constructor
   */
  constructor(container, width) {
    super(container);
    this.observers_ = new observers.Subject();
    this.show_ = false;
    this.width_ = width ? width : '75%';
    this.render();
  }

  get show() {
    return this.show_;
  }

  set show(value) {
    this.show_ = value;
    this.render();
  }

  /**
   * Listen to the `opening` event.
   *
   * @param {function(Element): void} callback the callback to call when the event is triggered.
   * @name onOpen
   * @function
   * @public
   */
  onOpen(callback) {
    this.observers_.register('opening', (el) => {
      if (callback) {
        callback(el);
      }
    });
  }

  /**
   * Listen to the `opened` event.
   *
   * @param {function(Element): void} callback the callback to call when the event is triggered.
   * @name onOpened
   * @function
   * @public
   */
  onOpened(callback) {
    this.observers_.register('opened', (el) => {
      if (callback) {
        callback(el);
      }
    });
  }

  /**
   * Listen to the `closing` event.
   *
   * @param {function(Element): void} callback the callback to call when the event is triggered.
   * @name onClose
   * @function
   * @public
   */
  onClose(callback) {
    this.observers_.register('closing', (el) => {
      if (callback) {
        callback(el);
      }
    });
  }

  _newElement() {
    return React.createElement(Drawer, {
      isOpen: this.show,
      size: this.width_,
      position: Position.RIGHT,
      onOpening: (el) => this.observers_.notify('opening', el),
      onOpened: (el) => this.observers_.notify('opened', el),
      onClose: () => this.show = false,
      onClosed: (el) => this.observers_.notify('closing', el),
    });
  }
}

/**
 * A skeleton to ease the creation of a minimal Blueprintjs tabs element.
 *
 * @memberOf module:blueprintjs
 * @extends {blueprintjs.Blueprintjs}
 * @type {blueprintjs.MinimalTabs}
 */
blueprintjs.MinimalTabs = class extends blueprintjs.Blueprintjs {

  /**
   * @param {Element} container the parent element.
   * @constructor
   */
  constructor(container) {
    super(container);
    this.observers_ = new observers.Subject();
    this.tabs_ = [];
    this.render();
  }

  /**
   * Add a single tab to the nav bar.
   *
   * @param {string} name the tab name.
   * @param {Element} panel the tab content.
   * @name addTab
   * @function
   * @public
   */
  addTab(name, panel) {
    this.tabs_.push({
      name: name, panel: panel, disabled: false, is_selected: false,
    });
    this.render();
  }

  /**
   * Remove a single tab from the nav bar.
   *
   * @param {string} name the tab name.
   * @name removeTab
   * @function
   * @public
   */
  removeTab(name) {
    this.tabs_ = this.tabs_.filter(tab => tab.name !== name);
    this.render();
  }

  /**
   * Select the tab to display.
   *
   * @param {string} name the tab name.
   * @name selectTab
   * @function
   * @public
   */
  selectTab(name) {
    let selectedTab = null;
    this.tabs_.forEach(tab => {
      if (tab.name !== name) {
        tab.is_selected = false;
      } else {
        tab.is_selected = true;
        selectedTab = tab;
      }
    });
    this.render();
    if (selectedTab) {
      this.observers_.notify('selection-change', selectedTab.name, selectedTab.panel);
    }
  }

  /**
   * Listen to the `selection-change` event.
   *
   * @param {function(string, Element): void} callback the callback to call when the event is triggered.
   * @name onSelectionChange
   * @function
   * @public
   */
  onSelectionChange(callback) {
    this.observers_.register('selection-change', (tabName, tabBody) => {
      // console.log('Selected tab is ' + tabName);
      if (callback) {
        callback(tabName, tabBody);
      }
    });
  }

  _newTab(tab) {
    return React.createElement(Tab, {
      id: tab.name, title: tab.name, panel: null, disabled: tab.disabled,
    });
  }

  _newElement() {
    const selectedTab = this.tabs_.find(tab => tab.is_selected);
    return React.createElement(Tabs, {
      id: 'tabs',
      children: this.tabs_.map(tab => this._newTab(tab)),
      selectedTabId: selectedTab ? selectedTab.name : null,
      onChange: (newTabId, oldTabId) => this.selectTab(newTabId)
    });
  }
}

/**
 * A skeleton to ease the creation of a minimal Blueprintjs spinner element.
 *
 * @memberOf module:blueprintjs
 * @extends {blueprintjs.Blueprintjs}
 * @type {blueprintjs.MinimalSpinner}
 */
blueprintjs.MinimalSpinner = class extends blueprintjs.Blueprintjs {

  /**
   * @param {Element} container the parent element.
   * @param {string} size the spinner size in {'small', 'standard', 'large'}
   * @constructor
   */
  constructor(container, size) {
    super(container);
    this.value_ = null;
    if (size === 'small') {
      this.size_ = SpinnerSize.SMALL;
    } else if (size === 'large') {
      this.size_ = SpinnerSize.LARGE;
    } else {
      this.size_ = SpinnerSize.STANDARD;
    }
    this.render();
  }

  /**
   * Represents how far along an operation is.
   *
   * @param {number} value a value between 0 and 1 (inclusive) representing how far along an operation is.
   * @name advance
   * @function
   * @public
   */
  advance(value) {
    this.value_ = value;
    this.render();
  }

  _newElement() {
    return React.createElement(Spinner, {
      value: this.value_, size: this.size_,
    });
  }
}

/**
 * A skeleton to ease the creation of a minimal Blueprintjs switch element.
 *
 * @memberOf module:blueprintjs
 * @extends {blueprintjs.Blueprintjs}
 * @type {blueprintjs.MinimalSwitch}
 */
blueprintjs.MinimalSwitch = class extends blueprintjs.Blueprintjs {

  /**
   * @param {Element} container the parent element.
   * @param {boolean} checked true iif the control should initially be checked, false otherwise (optional).
   * @param {string} label the switch label (optional).
   * @param {string} labelPosition the switch label position (in {left, right}) in respect to the element (optional).
   * @param {string} labelChecked the text to display inside the switch indicator when checked (optional).
   * @param {string} labelUnchecked the text to display inside the switch indicator when unchecked (optional).
   * @constructor
   */
  constructor(container, checked, label, labelPosition, labelChecked, labelUnchecked) {
    super(container);
    this.checked_ = checked;
    this.label_ = label;
    this.switchPosition_ = labelPosition === 'left' ? Alignment.RIGHT : Alignment.LEFT;
    this.labelChecked_ = labelChecked;
    this.labelUnchecked_ = labelUnchecked;
    this.observers_ = new observers.Subject();
    this.disabled_ = false;
    this.render();
  }

  get disabled() {
    return this.disabled_;
  }

  set disabled(value) {
    this.disabled_ = value;
    this.render();
  }

  get checked() {
    return this.checked_;
  }

  set checked(value) {
    this.checked_ = value;
    this.render();
  }

  /**
   * Listen to the `selection-change` event.
   *
   * @param {function(boolean): void} callback the callback to call when the event is triggered.
   * @name onSelectionChange
   * @function
   * @public
   */
  onSelectionChange(callback) {
    this.observers_.register('selection-change', (value) => {
      // console.log('Selected option is ' + (value ? 'checked' : 'unchecked'));
      if (callback) {
        callback(value ? 'checked' : 'unchecked');
      }
    });
  }

  _newElement() {
    return React.createElement(Switch, {
      disabled: this.disabled_,
      checked: this.checked_,
      label: this.label_,
      alignIndicator: this.switchPosition_,
      innerLabel: this.labelUnchecked_,
      innerLabelChecked: this.labelChecked_,
      onChange: () => {
        this.checked = !this.checked;
        this.observers_.notify('selection-change', this.checked);
      },
    });
  }
}

/**
 * A skeleton to ease the creation of a minimal Blueprintjs toast element.
 *
 * @memberOf module:blueprintjs
 * @extends {blueprintjs.Blueprintjs}
 * @type {blueprintjs.MinimalToast}
 */
blueprintjs.MinimalToast = class extends blueprintjs.Blueprintjs {

  /**
   * @param {Element} container the parent element.
   * @param {string} message the message to display.
   * @param {string} intent the message intent in {none, primary, success, warning, danger} (optional).
   * @param {number} timeout the number of milliseconds to wait before automatically dismissing the toast (optional).
   * @constructor
   */
  constructor(container, message, intent, timeout) {
    super(container);
    this.timeout_ = timeout;
    this.message_ = message;
    if (intent === 'primary') {
      this.intent_ = Intent.PRIMARY;
      this.icon_ = null;
    } else if (intent === 'success') {
      this.intent_ = Intent.SUCCESS;
      this.icon_ = 'tick';
    } else if (intent === 'warning') {
      this.intent_ = Intent.WARNING;
      this.icon_ = 'warning-sign';
    } else if (intent === 'danger') {
      this.intent_ = Intent.DANGER;
      this.icon_ = 'warning-sign';
    } else {
      this.intent_ = Intent.NONE;
      this.icon_ = null;
    }
    this.observers_ = new observers.Subject();
    this.render();
  }

  /**
   * Listen to the `dismiss` event.
   *
   * @param {function(void): void} callback the callback to call when the event is triggered.
   * @name onDismiss
   * @function
   * @public
   */
  onDismiss(callback) {
    this.observers_.register('dismiss', (self) => {
      // console.log('Toast dismissed!');
      if (callback) {
        callback();
      }
    });
  }

  _newElement() {
    return React.createElement(Toast, {
      intent: this.intent_,
      icon: this.icon_,
      message: React.createElement('div', {}, this.message_),
      timeout: this.timeout_,
      onDismiss: () => this.observers_.notify('dismiss', this),
    });
  }
}

/**
 * A skeleton to ease the creation of a minimal Blueprintjs toaster element.
 *
 * @memberOf module:blueprintjs
 * @extends {blueprintjs.Blueprintjs}
 * @type {blueprintjs.MinimalToaster}
 */
blueprintjs.MinimalToaster = class extends blueprintjs.Blueprintjs {

  /**
   * @param {Element} container the parent element.
   * @constructor
   */
  constructor(container) {
    super(container);
    this.toasts_ = [];
    this.render();
  }

  /**
   * Create and display a new toast.
   *
   * @param {string} message the message to display.
   * @param {string} intent the message intent in {none, primary, success, warning, danger} (optional).
   * @param {number} timeout the number of milliseconds to wait before automatically dismissing the toast (optional).
   * @name toast
   * @function
   * @public
   */
  toast(message, intent, timeout) {
    const toast = new blueprintjs.MinimalToast(this.container, message, intent, timeout);
    toast.el_ = toast._newElement();
    toast.onDismiss(() => {
      this.toasts_ = this.toasts_.filter(t => t !== toast);
      this.render();
    });
    this.toasts_.push(toast);
    this.render();
  }

  _newElement() {
    return React.createElement(Toaster, {
      children: this.toasts_.map(toast => toast.el_), position: Position.TOP,
    });
  }
}

/**
 * A skeleton to ease the creation of a minimal Blueprintjs card element.
 *
 * @memberOf module:blueprintjs
 * @extends {blueprintjs.Blueprintjs}
 * @type {blueprintjs.MinimalCard}
 */
blueprintjs.MinimalCard = class extends blueprintjs.Blueprintjs {

  /**
   * @param {Element} container the parent element.
   * @param {Element} body the card body.
   * @constructor
   */
  constructor(container, body) {
    super(container);
    this.elevation_ = 0;
    this.interactive_ = false;
    this.observers_ = new observers.Subject();
    this.body_ = React.createElement('div', {
      ref: React.createRef(),
    });
    this.render(); // this.body_ must be rendered first!
    this.body_.ref.current.appendChild(body);
    this.render();
  }

  get elevation() {
    return this.elevation_;
  }

  set elevation(value) {
    this.elevation_ = !value ? 0 : value > 4 ? 4 : value;
    this.render();
  }

  get interactive() {
    return this.interactive_;
  }

  set interactive(value) {
    this.interactive_ = value;
    this.render();
  }

  /**
   * Listen to the `click` event.
   *
   * @param {function(void): void} callback the callback to call when the event is triggered.
   * @name onClick
   * @function
   * @public
   */
  onClick(callback) {
    this.observers_.register('click', (self) => {
      // console.log('Card clicked!');
      if (callback) {
        callback();
      }
    });
  }

  _newElement() {
    return React.createElement(Card, {
      children: [this.body_],
      elevation: this.elevation_,
      interactive: this.interactive_,
      onClick: () => this.observers_.notify('click', this),
    });
  }
}

/**
 * A skeleton to ease the creation of a minimal Blueprintjs icon element.
 *
 * @memberOf module:blueprintjs
 * @extends {blueprintjs.Blueprintjs}
 * @type {blueprintjs.MinimalIcon}
 */
blueprintjs.MinimalIcon = class extends blueprintjs.Blueprintjs {

  /**
   * @param {Element} container the parent element.
   * @param {string} icon the icon name.
   * @param {string} intent the icon intent in {none, primary, success, warning, danger} (optional).
   * @constructor
   */
  constructor(container, icon, intent) {
    super(container);
    this.observers_ = new observers.Subject();
    this.icon_ = icon;
    this.size_ = 20;
    if (intent === 'primary') {
      this.intent_ = Intent.PRIMARY;
    } else if (intent === 'success') {
      this.intent_ = Intent.SUCCESS;
    } else if (intent === 'warning') {
      this.intent_ = Intent.WARNING;
    } else if (intent === 'danger') {
      this.intent_ = Intent.DANGER;
    } else {
      this.intent_ = Intent.NONE;
    }
    this.render();
  }

  get icon() {
    return this.icon_;
  }

  set icon(value) {
    this.icon_ = value;
    this.render();
  }

  get size() {
    return this.size_;
  }

  set size(value) {
    this.size_ = value;
    this.render();
  }

  get intent() {
    return this.intent_;
  }

  set intent(value) {
    this.intent_ = value;
    this.render();
  }

  /**
   * Listen to the `click` event.
   *
   * @param {function(void): void} callback the callback to call when the event is triggered.
   * @name onClick
   * @function
   * @public
   */
  onClick(callback) {
    this.observers_.register('click', (self) => {
      // console.log('Icon clicked!');
      if (callback) {
        callback();
      }
    });
  }

  _newElement() {
    return React.createElement(Icon, {
      icon: this.icon_, size: this.size_, intent: this.intent_, onClick: () => this.observers_.notify('click', this),
    });
  }
}

/**
 * A skeleton to ease the creation of a minimal Blueprintjs checkbox element.
 *
 * @memberOf module:blueprintjs
 * @extends {blueprintjs.Blueprintjs}
 * @type {blueprintjs.MinimalCheckbox}
 */
blueprintjs.MinimalCheckbox = class extends blueprintjs.Blueprintjs {

  /**
   * @param {Element} container the parent element.
   * @param {boolean} checked true iif the control should initially be checked, false otherwise (optional).
   * @param {string} label the switch label (optional).
   * @param {string} labelPosition the switch label position (in {left, right}) in respect to the element (optional).
   * @constructor
   */
  constructor(container, checked, label, labelPosition) {
    super(container);
    this.observers_ = new observers.Subject();
    this.checked_ = checked;
    this.label_ = label;
    this.boxPosition_ = labelPosition === 'left' ? Alignment.RIGHT : Alignment.LEFT;
    this.disabled_ = false;
    this.render();
  }

  get checked() {
    return this.checked_;
  }

  set checked(value) {
    this.checked_ = value;
    this.render();
  }

  get disabled() {
    return this.disabled_;
  }

  set disabled(value) {
    this.disabled_ = value;
    this.render();
  }

  /**
   * Listen to the `selection-change` event.
   *
   * @param {function(string): void} callback the callback to call when the event is triggered.
   * @name onSelectionChange
   * @function
   * @public
   */
  onSelectionChange(callback) {
    this.observers_.register('selection-change', (value) => {
      // console.log('Selected option is ' + (value ? 'checked' : 'unchecked'));
      if (callback) {
        callback(value ? 'checked' : 'unchecked');
      }
    });
  }

  _newElement() {
    return React.createElement(Checkbox, {
      checked: this.checked_,
      disabled: this.disabled_,
      label: this.label_,
      alignIndicator: this.boxPosition_,
      onChange: () => {
        this.checked = !this.checked;
        this.observers_.notify('selection-change', this.checked);
      },
    });
  }
}

/**
 * A skeleton to ease the creation of a minimal Blueprintjs date element.
 *
 * @memberOf module:blueprintjs
 * @extends {blueprintjs.Blueprintjs}
 * @type {blueprintjs.MinimalDate}
 */
blueprintjs.MinimalDate = class extends blueprintjs.Blueprintjs {

  /**
   * @param {Element} container the parent element.
   * @param {string} format the date format (optional). Default is 'yyyy-MM-dd'.
   * @param {Date} minDate the earliest date the user can select (optional).
   * @param {Date} maxDate the latest date the user can select (optional).
   * @constructor
   */
  constructor(container, format, minDate, maxDate) {
    super(container);
    this.observers_ = new observers.Subject();
    this.value_ = null;
    this.disabled_ = false;
    this.format_ = format ? format : 'yyyy-MM-dd';
    this.fillContainer_ = true;
    this.shortcuts_ = false;
    this.showActionsBar_ = false;
    this.minDate_ = minDate ? minDate : sub(new Date(), {years: 10});
    this.maxDate_ = maxDate ? maxDate : add(new Date(), {years: 10});
    this.render();
  }

  get date() {
    return this.value_;
  }

  set date(value) {
    this.value_ = value;
    this.render();
  }

  get disabled() {
    return this.disabled_;
  }

  set disabled(value) {
    this.disabled_ = value;
    this.render();
  }

  get shortcuts() {
    return this.shortcuts_;
  }

  set shortcuts(value) {
    this.shortcuts_ = value;
    this.render();
  }

  get showActionsBar() {
    return this.showActionsBar_;
  }

  set showActionsBar(value) {
    this.showActionsBar_ = value;
    this.render();
  }

  get fillContainer() {
    return this.fillContainer_;
  }

  set fillContainer(value) {
    this.fillContainer_ = value;
    this.render();
  }

  get minDate() {
    return this.minDate_;
  }

  set minDate(value) {
    this.minDate_ = value;
    this.render();
  }

  get maxDate() {
    return this.maxDate_;
  }

  set maxDate(value) {
    this.maxDate_ = value;
    this.render();
  }

  /**
   * Listen to the `selection-change` event.
   *
   * @param {function(string): void} callback the callback to call when the event is triggered.
   * @name onSelectionChange
   * @function
   * @public
   */
  onSelectionChange(callback) {
    this.observers_.register('selection-change', (value) => {
      // console.log('Selected date is ' + value);
      if (callback) {
        callback(value);
      }
    });
  }

  _newElement() {
    return React.createElement(DateInput2, {
      formatDate: (date) => format(date, this.format_),
      parseDate: (str) => parse(str, this.format_, new Date()),
      value: this.date,
      disabled: this.disabled,
      placeholder: this.format_,
      fill: this.fillContainer,
      minDate: this.minDate,
      maxDate: this.maxDate,
      shortcuts: this.shortcuts,
      showActionsBar: this.showActionsBar,
      showTimezoneSelect: this._showTimezone(),
      disableTimezoneSelect: this._disableTimezone(),
      timePrecision: this._timePrecision(),
      defaultTimezone: this._defaultTimezone(),
      onChange: (value) => {
        this.date = value;
        this.observers_.notify('selection-change', this.date);
      }
    });
  }

  /* Time-specific functions */

  _showTimezone() {
    return false;
  }

  _timePrecision() {
    return null;
  }

  _defaultTimezone() {
    return null;
  }

  _disableTimezone() {
    return true;
  }
}

/**
 * A skeleton to ease the creation of a minimal Blueprintjs datetime element.
 *
 * @memberOf module:blueprintjs
 * @extends {blueprintjs.Blueprintjs}
 * @type {blueprintjs.MinimalDatetime}
 */
blueprintjs.MinimalDatetime = class extends blueprintjs.MinimalDate {

  /**
   * @param {Element} container the parent element.
   * @param {string} format the date format (optional). Default is 'yyyy-MM-dd HH:mm'.
   * @param {Date} minDate the earliest date the user can select (optional).
   * @param {Date} maxDate the latest date the user can select (optional).
   * @param {string} timePrecision the time precision in {'hours', 'minutes', 'seconds'} (optional). Default is 'minutes'.
   * @param {string} defaultTimezone the default time zone (optional). Default is 'UTC'.
   * @constructor
   */
  constructor(container, format, minDate, maxDate, timePrecision, defaultTimezone) {
    super(container, format ? format : 'yyyy-MM-dd HH:mm', minDate, maxDate);
    this.timePrecision_ = timePrecision === 'hours' ? TimePrecision.HOUR_24 : timePrecision === 'seconds'
      ? TimePrecision.SECOND : TimePrecision.MINUTE;
    this.defaultTimezone_ = defaultTimezone ? defaultTimezone : 'Etc/UTC';
    this.disableTimezone_ = false;
    this.render();
  }

  get disableTimezone() {
    return this.disableTimezone_;
  }

  set disableTimezone(value) {
    this.disableTimezone_ = value;
    this.render();
  }

  _showTimezone() {
    return true;
  }

  _timePrecision() {
    return this.timePrecision_;
  }

  _defaultTimezone() {
    return this.defaultTimezone_;
  }

  _disableTimezone() {
    return this.disableTimezone;
  }
}

/**
 * A skeleton to ease the creation of a minimal Blueprintjs date range element.
 *
 * @memberOf module:blueprintjs
 * @extends {blueprintjs.Blueprintjs}
 * @type {blueprintjs.MinimalDateRange}
 */
blueprintjs.MinimalDateRange = class extends blueprintjs.Blueprintjs {

  /**
   * @param {Element} container the parent element.
   * @param {string} format the date format (optional). Default is 'yyyy-MM-dd'.
   * @param {Date} minDate the earliest date the user can select (optional).
   * @param {Date} maxDate the latest date the user can select (optional).
   * @constructor
   */
  constructor(container, format, minDate, maxDate) {
    super(container);
    this.observers_ = new observers.Subject();
    this.disabled_ = false;
    this.shortcuts_ = true;
    this.dateMin_ = null;
    this.dateMax_ = null;
    this.format_ = format ? format : 'yyyy-MM-dd';
    this.minDate_ = minDate ? minDate : sub(new Date(), {years: 10});
    this.maxDate_ = maxDate ? maxDate : add(new Date(), {years: 10});
    this.render();
  }

  get disabled() {
    return this.disabled_;
  }

  set disabled(value) {
    this.disabled_ = value;
    this.render();
  }

  get shortcuts() {
    return this.shortcuts_;
  }

  set shortcuts(value) {
    this.shortcuts_ = value;
    this.render();
  }

  get dateMin() {
    return this.dateMin_;
  }

  set dateMin(value) {
    this.dateMin_ = value;
    this.render();
  }

  get dateMax() {
    return this.dateMax_;
  }

  set dateMax(value) {
    this.dateMax_ = value;
    this.render();
  }

  get minDate() {
    return this.minDate_;
  }

  set minDate(value) {
    this.minDate_ = value;
    this.render();
  }

  get maxDate() {
    return this.maxDate_;
  }

  set maxDate(value) {
    this.maxDate_ = value;
    this.render();
  }

  /**
   * Listen to the `selection-change` event.
   *
   * @param {function(Date, Date): void} callback the callback to call when the event is triggered.
   * @name onSelectionChange
   * @function
   * @public
   */
  onSelectionChange(callback) {
    this.observers_.register('selection-change', (range) => {
      // console.log('Selected range is ' + range);
      if (callback) {
        callback(range[0], range[1]);
      }
    });
  }

  _newElement() {
    return React.createElement(DateRangeInput2, {
      formatDate: (date) => format(date, this.format_),
      parseDate: (str) => parse(str, this.format_, new Date()),
      value: [this.dateMin, this.dateMax],
      disabled: this.disabled,
      placeholder: this.format_,
      shortcuts: this.shortcuts,
      minDate: this.minDate,
      maxDate: this.maxDate,
      onChange: (range) => {
        this.dateMin = range[0];
        this.dateMax = range[1];
        this.observers_.notify('selection-change', range);
      }
    });
  }
}

/**
 * A skeleton to ease the creation of a minimal Blueprintjs multiselect element.
 *
 * @memberOf module:blueprintjs
 * @extends {blueprintjs.Blueprintjs}
 * @type {blueprintjs.MinimalMultiSelect}
 */
blueprintjs.MinimalMultiSelect = class extends blueprintjs.Blueprintjs {

  /**
   * @param {Element} container the parent element.
   * @param {function(*): string} itemToText a function that maps an item to the text to be displayed (optional).
   * @param {function(*): string} itemToLabel a function that maps an item to the label to be displayed (optional).
   * @param {function(*): string} itemToTag a function that maps an item to the tag to be displayed (optional).
   * @param {function(string, *): boolean} itemPredicate a function that filters the internal list of items when user enters something in the input (optional).
   * @param {function(string): *} itemCreate a function that creates an item from a string (optional).
   * @constructor
   */
  constructor(container, itemToText, itemToLabel, itemToTag, itemPredicate, itemCreate) {
    super(container);
    this.itemToText_ = itemToText;
    this.itemToLabel_ = itemToLabel;
    this.itemToTag_ = itemToTag;
    this.itemPredicate_ = (query, item) => {
      if (itemPredicate) {
        return itemPredicate(query, item);
      }
      if (query && query !== '') {
        const txt = this.itemToText_ ? this.itemToText_(item) : item;
        return txt.trim().toLowerCase().indexOf(query.trim().toLowerCase()) >= 0;
      }
      return true;
    };
    this.itemCreate_ = itemCreate;
    this.observers_ = new observers.Subject();
    this.fillContainer_ = true;
    this.disabled_ = false;
    this.items_ = [];
    this.selectedItems_ = [];
    this.defaultText_ = 'Sélectionnez un élément...';
    this.noResults_ = 'Il n\'y a aucun résultat pour cette recherche.';
    this.render();
  }

  get fillContainer() {
    return this.fillContainer_;
  }

  set fillContainer(value) {
    this.fillContainer_ = value;
    this.render();
  }

  get disabled() {
    return this.disabled_;
  }

  set disabled(value) {
    this.disabled_ = value;
    this.render();
  }

  get items() {
    return this.items_;
  }

  set items(values) {
    this.items_ = values;
    this.render();
  }

  get selectedItems() {
    return this.selectedItems_;
  }

  set selectedItems(value) {
    this.selectedItems_ = value ? value : [];
    this.render();
  }

  get defaultText() {
    return this.defaultText_;
  }

  set defaultText(value) {
    this.defaultText_ = value;
    this.render();
  }

  get noResults() {
    return this.noResults_;
  }

  set noResults(value) {
    this.noResults_ = value;
    this.render();
  }

  /**
   * Listen to the `selection-change` event.
   *
   * @param {function(*): void} callback the callback to call when the event is triggered.
   * @name onSelectionChange
   * @function
   * @public
   */
  onSelectionChange(callback) {
    this.observers_.register('selection-change', (items) => {
      // console.log('Selected items are ', items);
      if (callback) {
        callback(items);
      }
    });
  }

  /**
   * Listen to the `filter-change` event.
   *
   * @param {function(*): void} callback the callback to call when the event is triggered.
   * @name onFilterChange
   * @function
   * @public
   */
  onFilterChange(callback) {
    this.observers_.register('filter-change', (query) => {
      // console.log('Query is ', query);
      if (callback) {
        callback(query);
      }
    });
  }

  _newElement() {
    return React.createElement(MultiSelect2, {
      fill: this.fillContainer,
      disabled: this.disabled,
      items: this.items,
      selectedItems: this.selectedItems,
      placeholder: this.defaultText,
      onQueryChange: (query) => {
        this.observers_.notify('filter-change', query);
      },
      onClear: () => {
        this.selectedItems_ = [];
        this.render();
        this.observers_.notify('selection-change', this.selectedItems);
      },
      itemPredicate: this.itemPredicate_,
      onItemSelect: (item) => {
        // If the user selects twice the same item, do not add it twice to the selection
        const pos = this.selectedItems.map(i => this.itemToText_ ? this.itemToText_(i) : i).indexOf(
          this.itemToText_ ? this.itemToText_(item) : item);
        if (pos !== 0 && pos <= -1) {
          this.selectedItems_.push(item);
          this.render();
          this.observers_.notify('selection-change', this.selectedItems);
        }
      },
      itemRenderer: (item, props) => {
        if (!props.modifiers.matchesPredicate) {
          return null;
        }
        return React.createElement(MenuItem, {
          key: props.index,
          selected: props.modifiers.active,
          text: this.itemToText_ ? this.itemToText_(item) : item,
          label: this.itemToLabel_ ? this.itemToLabel_(item) : '',
          onFocus: props.handleFocus,
          onClick: props.handleClick,
        });
      },
      tagRenderer: (item) => {
        return this.itemToTag_ ? this.itemToTag_(item) : item;
      },
      onRemove: (tag, index) => {
        this.selectedItems_.splice(index, 1);
        this.render();
        this.observers_.notify('selection-change', this.selectedItems);
      },
      noResults: React.createElement(MenuItem, {
        text: this.noResults, disabled: true,
      }),
      popoverProps: {
        matchTargetWidth: true,
      },
      resetOnSelect: !!this.itemCreate_,
      createNewItemFromQuery: this.itemCreate_,
      createNewItemRenderer: (query, active, handleClick) => {
        return React.createElement(MenuItem, {
          icon: 'add', selected: active, text: query, onClick: handleClick,
        });
      },
    });
  }
}

/**
 * A skeleton to ease the creation of a minimal Blueprintjs suggest element.
 *
 * @memberOf module:blueprintjs
 * @extends {blueprintjs.Blueprintjs}
 * @type {blueprintjs.MinimalSuggest}
 */
blueprintjs.MinimalSuggest = class extends blueprintjs.Blueprintjs {

  /**
   * @param {Element} container the parent element.
   * @param {function(*): string} itemToText a function that maps an item to the text to be displayed (optional).
   * @param {function(*): string} itemToLabel a function that maps an item to the label to be displayed (optional).
   * @param {function(*): boolean} itemPredicate a function that filters the internal list of items when user enters something in the input (optional).
   * @constructor
   */
  constructor(container, itemToText, itemToLabel, itemPredicate) {
    super(container);
    this.itemToText_ = itemToText;
    this.itemToLabel_ = itemToLabel;
    this.itemPredicate_ = (query, item) => {
      if (itemPredicate) {
        return itemPredicate(query, item);
      }
      if (query && query !== '') {
        const txt = this.itemToText_ ? this.itemToText_(item) : item;
        return txt.trim().toLowerCase().indexOf(query.trim().toLowerCase()) >= 0;
      }
      return true;
    };
    this.observers_ = new observers.Subject();
    this.fillContainer_ = true;
    this.disabled_ = false;
    this.items_ = [];
    this.selectedItem_ = null;
    this.defaultText_ = 'Saisissez un caractère...';
    this.noResults_ = 'Il n\'y a aucun résultat pour cette recherche.';
    this.render();
  }

  get fillContainer() {
    return this.fillContainer_;
  }

  set fillContainer(value) {
    this.fillContainer_ = value;
    this.render();
  }

  get disabled() {
    return this.disabled_;
  }

  set disabled(value) {
    this.disabled_ = value;
    this.render();
  }

  get items() {
    return this.items_;
  }

  set items(values) {
    this.items_ = values;
    this.render();
  }

  get selectedItem() {
    return this.selectedItem_;
  }

  set selectedItem(value) {
    this.selectedItem_ = value ? value : null;
    this.render();
  }

  get defaultText() {
    return this.defaultText_;
  }

  set defaultText(value) {
    this.defaultText_ = value;
    const input = this.container.querySelector('input');
    if (input) {
      input.placeholder = this.defaultText_;
    }
  }

  get noResults() {
    return this.noResults_;
  }

  set noResults(value) {
    this.noResults_ = value;
    this.render();
  }

  /**
   * Listen to the `selection-change` event.
   *
   * @param {function(*): void} callback the callback to call when the event is triggered.
   * @name onSelectionChange
   * @function
   * @public
   */
  onSelectionChange(callback) {
    this.observers_.register('selection-change', (item) => {
      // console.log('Selected item is ', item);
      if (callback) {
        callback(item);
      }
    });
  }

  /**
   * Listen to the `filter-change` event.
   *
   * @param {function(*): void} callback the callback to call when the event is triggered.
   * @name onFilterChange
   * @function
   * @public
   */
  onFilterChange(callback) {
    this.observers_.register('filter-change', (query) => {
      // console.log('Query is ', query);
      if (callback) {
        callback(query);
      }
    });
  }

  _newElement() {
    return React.createElement(Suggest2, {
      fill: this.fillContainer,
      disabled: this.disabled,
      items: this.items,
      selectedItem: this.selectedItem,
      onQueryChange: (query) => {
        this.observers_.notify('filter-change', query);
      },
      inputValueRenderer: item => this.itemToText_ ? this.itemToText_(item) : item,
      onItemSelect: (item) => {
        this.selectedItem_ = item;
        this.render();
        this.observers_.notify('selection-change', this.selectedItem);
      },
      itemPredicate: this.itemPredicate_,
      itemRenderer: (item, props) => {
        if (!props.modifiers.matchesPredicate) {
          return null;
        }
        return React.createElement(MenuItem, {
          key: props.index,
          selected: props.modifiers.active,
          text: this.itemToText_ ? this.itemToText_(item) : item,
          label: this.itemToLabel_ ? this.itemToLabel_(item) : '',
          onFocus: props.handleFocus,
          onClick: props.handleClick,
        });
      },
      noResults: React.createElement(MenuItem, {
        text: this.noResults, disabled: true,
      }),
      popoverProps: {
        matchTargetWidth: true,
      }
    });
  }
}

/**
 * A skeleton to ease the creation of a minimal Blueprintjs file input element.
 *
 * @memberOf module:blueprintjs
 * @extends {blueprintjs.Blueprintjs}
 * @type {blueprintjs.MinimalFileInput}
 */
blueprintjs.MinimalFileInput = class extends blueprintjs.Blueprintjs {

  /**
   * @param {Element} container the parent element.
   * @param {boolean} multiple true iif the user must be able to select one or more files.
   * @constructor
   */
  constructor(container, multiple) {
    super(container);
    this.observers_ = new observers.Subject();
    this.disabled_ = false;
    this.text_ = null;
    this.buttonText_ = null;
    this.fill_ = true;
    this.multiple_ = multiple === true;
    this.render();
  }

  get disabled() {
    return this.disabled_;
  }

  set disabled(value) {
    this.disabled_ = value;
    this.render();
  }

  get fill() {
    return this.fill_;
  }

  set fill(value) {
    this.fill_ = value;
    this.render();
  }

  get text() {
    return this.text_;
  }

  set text(value) {
    this.text_ = value;
    this.render();
  }

  get buttonText() {
    return this.buttonText_;
  }

  set buttonText(value) {
    this.buttonText_ = value;
    this.render();
  }

  get multiple() {
    return this.multiple_;
  }

  set multiple(value) {
    this.multiple_ = value;
    this.render();
  }

  /**
   * Listen to the `selection-change` event.
   *
   * @param {function(*): void} callback the callback to call when the event is triggered.
   * @name onSelectionChange
   * @function
   * @public
   */
  onSelectionChange(callback) {
    this.observers_.register('selection-change', (file) => {
      // console.log('Selected file is ', file);
      if (callback) {
        callback(file);
      }
    });
  }

  _newElement() {
    const props = {};
    if (this.multiple) {
      props.multiple = 'multiple';
    }
    return React.createElement(FileInput, {
      inputProps: props,
      disabled: this.disabled,
      text: this.text,
      buttonText: this.buttonText,
      fill: this.fill,
      onInputChange: (el) => {
        this.text = el.target.files[0].name;
        this.render();
        this.observers_.notify('selection-change', this.multiple ? el.target.files : el.target.files[0]);
      },
    });
  }
}

/**
 * A skeleton to ease the creation of a minimal Blueprintjs radio group element.
 *
 * @memberOf module:blueprintjs
 * @extends {blueprintjs.Blueprintjs}
 * @type {blueprintjs.MinimalRadioGroup}
 */
blueprintjs.MinimalRadioGroup = class extends blueprintjs.Blueprintjs {

  /**
   * @param {Element} container the parent element.
   * @param {string} label the group label (optional).
   * @param {boolean} inline true iif the radio buttons are to be displayed inline horizontally, false otherwise. (optional).
   * @constructor
   */
  constructor(container, label, inline) {
    super(container);
    this.label_ = label;
    this.inline_ = inline;
    this.observers_ = new observers.Subject();
    this.disabled_ = false;
    this.items_ = [];
    this.selectedItem_ = null;
    this.render();
  }

  get disabled() {
    return this.disabled_;
  }

  set disabled(value) {
    this.disabled_ = value;
    this.render();
  }

  get items() {
    return this.items_;
  }

  set items(values) {
    this.items_ = values;
    this.render();
  }

  get selectedItem() {
    return this.selectedItem_;
  }

  set selectedItem(value) {
    this.selectedItem_ = value;
    this.render();
  }

  /**
   * Listen to the `selection-change` event.
   *
   * @param {function(string): void} callback the callback to call when the event is triggered.
   * @name onSelectionChange
   * @function
   * @public
   */
  onSelectionChange(callback) {
    this.observers_.register('selection-change', (value) => {
      // console.log('Selected option is ', value);
      if (callback) {
        callback(value);
      }
    });
  }

  _newElement() {
    return React.createElement(RadioGroup, {
      label: this.label_,
      inline: this.inline_,
      disabled: this.disabled,
      options: this.items,
      selectedValue: this.selectedItem,
      onChange: (event) => {
        const selection = this.items.find(item => item.value === event.currentTarget.value);
        if (selection) {
          this.selectedItem = selection.value;
          this.observers_.notify('selection-change', selection);
        }
      },
    });
  }
}

/**
 * A skeleton to ease the creation of a minimal Blueprintjs text input element.
 *
 * @memberOf module:blueprintjs
 * @extends {blueprintjs.Blueprintjs}
 * @type {blueprintjs.MinimalTextInput}
 */
blueprintjs.MinimalTextInput = class extends blueprintjs.Blueprintjs {

  /**
   * @param {Element} container the parent element.
   * @param {string} defaultValue the input default value (optional).
   * @param {string} icon the icon name (optional).
   * @param {string} intent the input intent in {none, primary, success, warning, danger} (optional).
   *
   * @constructor
   */
  constructor(container, defaultValue, icon, intent) {
    super(container);
    this.defaultValue_ = defaultValue;
    this.icon_ = icon;
    this.intent_ = intent;
    this.observers_ = new observers.Subject();
    this.id_ = 'i' + Math.random().toString(36).substring(2, 12);
    this.disabled_ = false;
    this.fillContainer_ = true;
    this.placeholder_ = null;
    this.render();
  }

  get icon() {
    return this.icon_;
  }

  set icon(value) {
    this.icon_ = value;
    this.render();
  }

  get intent() {
    return this.intent_;
  }

  set intent(value) {
    this.intent_ = value;
    this.render();
  }

  get fillContainer() {
    return this.fillContainer_;
  }

  set fillContainer(value) {
    this.fillContainer_ = value;
    this.render();
  }

  get disabled() {
    return this.disabled_;
  }

  set disabled(value) {
    this.disabled_ = value;
    this.render();
  }

  get placeholder() {
    return this.placeholder_;
  }

  set placeholder(value) {
    this.placeholder_ = value;
    this.render();
  }

  get value() {
    return document.getElementById(this.id_).value;
  }

  _newElement() {
    return React.createElement(InputGroup, {
      id: this.id_,
      disabled: this.disabled,
      placeholder: this.placeholder,
      defaultValue: this.defaultValue_,
      fill: this.fillContainer,
      leftIcon: this.icon,
      intent: this.intent,
    });
  }
}

/**
 * A skeleton to ease the creation of a minimal Blueprintjs text input element.
 *
 * @memberOf module:blueprintjs
 * @extends {blueprintjs.Blueprintjs}
 * @type {blueprintjs.MinimalNumericInput}
 */
blueprintjs.MinimalNumericInput = class extends blueprintjs.Blueprintjs {

  /**
   * @param {Element} container the parent element.
   * @param {number} min the minimum value.
   * @param {number} max the maximum value.
   * @param {number} increment the internal increment.
   * @param {string} defaultValue the input default value (optional).
   * @param {string} icon the icon name (optional).
   * @param {string} intent the input intent in {none, primary, success, warning, danger} (optional).
   *
   * @constructor
   */
  constructor(container, min, max, increment, defaultValue, icon, intent) {
    super(container);
    this.min_ = min;
    this.max_ = max;
    this.increment_ = increment;
    this.defaultValue_ = defaultValue;
    this.icon_ = icon;
    this.intent_ = intent;
    this.id_ = 'i' + Math.random().toString(36).substring(2, 12);
    this.observers_ = new observers.Subject();
    this.disabled_ = false;
    this.fillContainer_ = true;
    this.placeholder_ = null;
    this.render();
  }

  get icon() {
    return this.icon_;
  }

  set icon(value) {
    this.icon_ = value;
    this.render();
  }

  get intent() {
    return this.intent_;
  }

  set intent(value) {
    this.intent_ = value;
    this.render();
  }

  get fillContainer() {
    return this.fillContainer_;
  }

  set fillContainer(value) {
    this.fillContainer_ = value;
    this.render();
  }

  get disabled() {
    return this.disabled_;
  }

  set disabled(value) {
    this.disabled_ = value;
    this.render();
  }

  get placeholder() {
    return this.placeholder_;
  }

  set placeholder(value) {
    this.placeholder_ = value;
    this.render();
  }

  get value() {
    return document.getElementById(this.id_).value;
  }

  /**
   * Listen to the `value-change` event.
   *
   * @param {function(number): void} callback the callback to call when the event is triggered.
   * @name onValueChange
   * @function
   * @public
   */
  onValueChange(callback) {
    this.observers_.register('value-change', (value) => {
      // console.log('Selected value is ' + value);
      if (callback) {
        callback(value);
      }
    });
  }

  _newElement() {
    return React.createElement(NumericInput, {
      id: this.id_,
      min: this.min_,
      max: this.max_,
      stepSize: this.increment_,
      disabled: this.disabled,
      placeholder: this.placeholder,
      defaultValue: this.defaultValue_,
      fill: this.fillContainer,
      leftIcon: this.icon,
      intent: this.intent,
      onValueChange: (value) => {
        this.observers_.notify('value-change', value);
      }
    });
  }
}

/**
 * A skeleton to ease the creation of a minimal Blueprintjs button element.
 *
 * @memberOf module:blueprintjs
 * @extends {blueprintjs.Blueprintjs}
 * @type {blueprintjs.MinimalButton}
 */
blueprintjs.MinimalButton = class extends blueprintjs.Blueprintjs {

  /**
   * @param {Element} container the parent element.
   * @param {string} label the switch label.
   * @param {string} labelPosition the switch label position (in {left, center, right}) in respect to the element (optional).
   * @param {string} leftIcon the left icon name (optional).
   * @param {string} rightIcon the right icon name (optional).
   * @param {string} intent the input intent in {none, primary, success, warning, danger} (optional).
   *
   * @constructor
   */
  constructor(container, label, labelPosition, leftIcon, rightIcon, intent) {
    super(container);
    this.label_ = label;
    this.labelPosition_ = labelPosition === 'left' ? Alignment.LEFT : labelPosition === 'right' ? Alignment.RIGHT
      : Alignment.CENTER;
    this.leftIcon_ = leftIcon;
    this.rightIcon_ = rightIcon;
    this.intent_ = intent;
    this.observers_ = new observers.Subject();
    this.disabled_ = false;
    this.loading_ = false;
    this.fillContainer_ = true;
    this.render();
  }

  get leftIcon() {
    return this.leftIcon_;
  }

  set leftIcon(value) {
    this.leftIcon_ = value;
    this.render();
  }

  get rightIcon() {
    return this.rightIcon_;
  }

  set rightIcon(value) {
    this.rightIcon_ = value;
    this.render();
  }

  get intent() {
    return this.intent_;
  }

  set intent(value) {
    this.intent_ = value;
    this.render();
  }

  get fillContainer() {
    return this.fillContainer_;
  }

  set fillContainer(value) {
    this.fillContainer_ = value;
    this.render();
  }

  get disabled() {
    return this.disabled_;
  }

  set disabled(value) {
    this.disabled_ = value;
    this.render();
  }

  get loading() {
    return this.loading_;
  }

  set loading(value) {
    this.loading_ = value;
    this.render();
  }

  /**
   * Listen to the `click` event.
   *
   * @param {function(void): void} callback the callback to call when the event is triggered.
   * @name onClick
   * @function
   * @public
   */
  onClick(callback) {
    this.observers_.register('click', () => {
      // console.log('Clicked!');
      if (callback) {
        callback();
      }
    });
  }

  _newElement() {
    return React.createElement(Button, {
      text: this.label_,
      alignText: this.labelPosition_,
      disabled: this.disabled,
      fill: this.fillContainer,
      loading: this.loading,
      icon: this.leftIcon,
      rightIcon: this.rightIcon,
      intent: this.intent,
      onClick: () => {
        this.observers_.notify('click');
      }
    });
  }
}