Home Reference Source

scripts/django_cradmin/widgets/AbstractDataListWidget.js

import AbstractWidget from "ievv_jsbase/widget/AbstractWidget";
import makeCustomError from "ievv_jsbase/makeCustomError";


let CancelledDataRequest = makeCustomError('CancelledDataRequest');


export default class AbstractDataListWidget extends AbstractWidget {

  getDefaultConfig() {
    return {
      signalNameSpace: null,
      keyAttribute: 'id',
      multiselect: false,
      selectedKeys: [],
      minimumSearchStringLength: 0,
      initialSearchString: '',
      filters: {}
    };
  }

  constructor(element, widgetInstanceId) {
    super(element, widgetInstanceId);
    this._name = `${this.classPath}.${widgetInstanceId}`;
    this.logger = new window.ievv_jsbase_core.LoggerSingleton().getLogger(
      this._name);
    this._onSearchValueChangeSignal = this._onSearchValueChangeSignal.bind(this);
    this._onSetFiltersSignal = this._onSetFiltersSignal.bind(this);
    this._onPatchFiltersSignal = this._onPatchFiltersSignal.bind(this);
    this._onSelectItemSignal = this._onSelectItemSignal.bind(this);
    this._onDeSelectItemSignal = this._onDeSelectItemSignal.bind(this);
    this._onFocusSignal = this._onFocusSignal.bind(this);
    this._onBlurSignal = this._onBlurSignal.bind(this);
    this._onLoadMoreSignal = this._onLoadMoreSignal.bind(this);
    this._onLoadNextPageSignal = this._onLoadNextPageSignal.bind(this);
    this._onLoadPreviousPageSignal = this._onLoadPreviousPageSignal.bind(this);
    this._dataRequestId = 0;
    this._isLoadingDataList = false;
    if(this.config.signalNameSpace == null) {
      throw new Error('The signalNameSpace config is required.');
    }
    this.state = null;
    this.signalHandlersInitialized = false;
  }

  _objectToMap(object) {
    const map = new Map();
    for(let key of Object.keys(object)) {
      map.set(key, object[key]);
    }
    return map;
  }

  _initialize(state) {
    this._initializeSignalHandlers();
    this.signalHandlersInitialized = true;
    this.state = {
      data: {
        count: 0,
        next: null,
        previous: null,
        results: []
      },
      selectedItemsMap: new Map(),
      searchString: '',
      filtersMap: this._objectToMap(this.config.filters),
      focus: false,
      loading: false
    };
    this._sendDataListInitializedSignal();
    this.setState(state, true);
  }

  _makeItemMapFromArray(itemDataArray) {
    const itemMap = new Map();
    for(let itemData of itemDataArray) {
      itemMap.set(this._getKeyFromItemData(itemData), itemData);
    }
    return itemMap;
  }

  _requestItemDataForKeys(keys) {
    const promises = [];
    for(let key of keys) {
      promises.push(this.requestItemData(key));
    }
    return Promise.all(promises);
  }

  _loadInitialState() {
    const state = {
      searchString: this.config.initialSearchString
    };
    this._requestItemDataForKeys(this.config.selectedKeys)
      .then((selectedItemsArray) => {
        state.setSelectedItems = selectedItemsArray;
        this._initialize(state);
      })
      .catch((error) => {
        this.logger.error('Failed to load config.selectedKeys:', this.config.selectedKeys, '. Error:', error.toString());
        this._initialize(state);
      });
  }

  useAfterInitializeAllWidgets() {
    return true;
  }

  afterInitializeAllWidgets() {
    this._loadInitialState();
  }

  _initializeSignalHandlers() {
    new window.ievv_jsbase_core.SignalHandlerSingleton().addReceiver(
      `${this.config.signalNameSpace}.SearchValueChange`,
      this._name,
      this._onSearchValueChangeSignal
    );
    new window.ievv_jsbase_core.SignalHandlerSingleton().addReceiver(
      `${this.config.signalNameSpace}.SetFilters`,
      this._name,
      this._onSetFiltersSignal
    );
    new window.ievv_jsbase_core.SignalHandlerSingleton().addReceiver(
      `${this.config.signalNameSpace}.PatchFilters`,
      this._name,
      this._onPatchFiltersSignal
    );
    new window.ievv_jsbase_core.SignalHandlerSingleton().addReceiver(
      `${this.config.signalNameSpace}.SelectItem`,
      this._name,
      this._onSelectItemSignal
    );
    new window.ievv_jsbase_core.SignalHandlerSingleton().addReceiver(
      `${this.config.signalNameSpace}.DeSelectItem`,
      this._name,
      this._onDeSelectItemSignal
    );
    new window.ievv_jsbase_core.SignalHandlerSingleton().addReceiver(
      `${this.config.signalNameSpace}.Focus`,
      this._name,
      this._onFocusSignal
    );
    new window.ievv_jsbase_core.SignalHandlerSingleton().addReceiver(
      `${this.config.signalNameSpace}.Blur`,
      this._name,
      this._onBlurSignal
    );
    new window.ievv_jsbase_core.SignalHandlerSingleton().addReceiver(
      `${this.config.signalNameSpace}.LoadMore`,
      this._name,
      this._onLoadMoreSignal
    );
    new window.ievv_jsbase_core.SignalHandlerSingleton().addReceiver(
      `${this.config.signalNameSpace}.LoadNextPage`,
      this._name,
      this._onLoadNextPageSignal
    );
    new window.ievv_jsbase_core.SignalHandlerSingleton().addReceiver(
      `${this.config.signalNameSpace}.LoadPreviousPage`,
      this._name,
      this._onLoadPreviousPageSignal
    );
  }

  destroy() {
    if(this.signalHandlersInitialized) {
      new window.ievv_jsbase_core.SignalHandlerSingleton()
        .removeAllSignalsFromReceiver(this._name);
    }
  }

  _updateSearchStringStateChange(stateChange, stateChangesSet) {
    if(stateChange.searchString != undefined) {
      this.state.searchString = stateChange.searchString;
      if(stateChange.searchString.length >= this.config.minimumSearchStringLength) {
        stateChangesSet.add('searchString');
      } else {
        stateChange.data = {
          count: 0,
          next: null,
          previous: null,
          results: []
        }
      }
    }
  }

  _updateFiltersStateChange(stateChange, stateChangesSet) {
    if(stateChange.filtersMap != undefined) {
      this.state.filtersMap = stateChange.filtersMap;
      stateChangesSet.add('filters');
    }
    if(stateChange.filtersMapPatch != undefined) {
      for(let [filterKey, filterValue] of stateChange.filtersMapPatch) {
        this.state.filtersMap.set(filterKey, filterValue);
      }
      stateChangesSet.add('filters');
    }
  }

  _updateDataStateChange(stateChange, stateChangesSet) {
    if(stateChange.data != undefined) {
      this.state.data = stateChange.data;
      stateChangesSet.add('data');
    } else if(stateChange.appendData != undefined) {
      for(let itemData of stateChange.appendData.results) {
        this.state.data.results.push(itemData);
      }
      this.state.data.count = stateChange.appendData.count;
      this.state.data.next = stateChange.appendData.next;
      this.state.data.previous = stateChange.appendData.previous;
      stateChangesSet.add('data');
    }
  }

  _updateSelectionStateChange(stateChange, stateChangesSet) {
    if(stateChange.addSelectedItem != undefined) {
      if(!this.config.multiselect) {
        this.state.selectedItemsMap.clear();
      }
      this.state.selectedItemsMap.set(
        this._getKeyFromItemData(stateChange.addSelectedItem), stateChange.addSelectedItem);
      stateChangesSet.add('selection');
    }
    if(stateChange.removeSelectedItem != undefined) {
      if(this.config.multiselect) {
        this.state.selectedItemsMap.delete(
          this._getKeyFromItemData(stateChange.removeSelectedItem));
      } else {
        this.state.selectedItemsMap.clear();
      }
      stateChangesSet.add('selection');
    }
    if(stateChange.clearSelectedKeys != undefined) {
      this.state.selectedItemsMap.clear();
      stateChangesSet.add('selection');
    }
    if(stateChange.setSelectedItems != undefined) {
      this.state.selectedItemsMap = this._makeItemMapFromArray(stateChange.setSelectedItems);
      stateChangesSet.add('selection');
    }
  }

  _updateFocusStateChange(stateChange, stateChangesSet) {
    if(stateChange.focus != undefined && this.state.focus != stateChange.focus) {
      this.state.focus = stateChange.focus;
      stateChangesSet.add('focus');
    }
  }

  _updateLoadingStateChange(stateChange, stateChangesSet) {
    if(stateChange.loading != undefined && this.state.loading != stateChange.loading) {
      this.state.loading = stateChange.loading;
      stateChangesSet.add('loading');
    }
  }

  _hasAnyFiltersOrSearchString() {
    return this.state.searchString != '' && this.state.filtersMap.size == 0;
  }

  setState(stateChange, initial=false) {
    const stateChangesSet = new Set();
    this._updateSearchStringStateChange(stateChange, stateChangesSet);
    this._updateFiltersStateChange(stateChange, stateChangesSet);
    this._updateDataStateChange(stateChange, stateChangesSet);
    this._updateSelectionStateChange(stateChange, stateChangesSet);
    this._updateFocusStateChange(stateChange, stateChangesSet);
    this._updateLoadingStateChange(stateChange, stateChangesSet);

    if(stateChangesSet.has('data')) {
      if(!this._hasAnyFiltersOrSearchString()) {
        if(this.state.data.count == 0) {
          this.state.isEmpty = true;
          this._sendIsEmpyDataListSignal();
        } else if(this.state.isEmpty || this.state.isEmpty == undefined) {
          this.state.isEmpty = false;
          this._sendNotEmpyDataListSignal();
        }
      }
      this._sendDataChangeSignal();
    }
    if(stateChangesSet.has('selection')) {
      this._sendSelectionChangeSignal();
    }
    if(stateChangesSet.has('searchString') || stateChangesSet.has('filters')) {
      this._requestDataListAndRefresh(this.makeRequestDataListOptions());
    }
    if(stateChangesSet.has('loading')) {
      this._sendLoadingStateChangeSignal();
    }
    if(stateChangesSet.has('filters')) {
      this._sendFiltersChangeSignal();
    }
    this._sendStateChangeSignal(stateChangesSet);
  }

  _sendIsEmpyDataListSignal() {
    new window.ievv_jsbase_core.SignalHandlerSingleton().send(
      `${this.config.signalNameSpace}.IsEmpyDataList`
    );
  }

  _sendNotEmpyDataListSignal() {
    new window.ievv_jsbase_core.SignalHandlerSingleton().send(
      `${this.config.signalNameSpace}.NotEmpyDataList`
    );
  }

  _sendDataChangeSignal() {
    new window.ievv_jsbase_core.SignalHandlerSingleton().send(
      `${this.config.signalNameSpace}.DataChange`,
      this.state.data
    );
  }

  _sendFiltersChangeSignal() {
    new window.ievv_jsbase_core.SignalHandlerSingleton().send(
      `${this.config.signalNameSpace}.FiltersChange`,
      this.state.filtersMap
    );
  }

  _sendSelectionChangeSignal() {
    new window.ievv_jsbase_core.SignalHandlerSingleton().send(
      `${this.config.signalNameSpace}.SelectionChange`,
      {selectedItemsMap: this.state.selectedItemsMap}
    );
  }

  _sendLoadingStateChangeSignal() {
    new window.ievv_jsbase_core.SignalHandlerSingleton().send(
      `${this.config.signalNameSpace}.LoadingStateChange`,
      this.state.loading
    );
  }

  _sendLostFocusSignal() {
    new window.ievv_jsbase_core.SignalHandlerSingleton().send(
      `${this.config.signalNameSpace}.LostFocus`,
      this.state.loading
    );
  }

  _sendStateChangeSignal(stateChanges) {
    new window.ievv_jsbase_core.SignalHandlerSingleton().send(
      `${this.config.signalNameSpace}.StateChange`,
      {state: this.state, stateChanges: stateChanges}
    );
  }

  _sendDataListInitializedSignal() {
    new window.ievv_jsbase_core.SignalHandlerSingleton().send(
      `${this.config.signalNameSpace}.DataListInitialized`,
      this.state
    );
  }

  _onSearchValueChangeSignal(receivedSignalInfo) {
    this.logger.debug('Received:', receivedSignalInfo.toString());
    this.setState({
      searchString: receivedSignalInfo.data
    });
  }

  _onSetFiltersSignal(receivedSignalInfo) {
    this.logger.debug('Received:', receivedSignalInfo.toString());
    this.setState({
      filtersMap: receivedSignalInfo.data
    });
  }

  _onPatchFiltersSignal(receivedSignalInfo) {
    this.logger.debug('Received:', receivedSignalInfo.toString());
    this.setState({
      filtersMapPatch: receivedSignalInfo.data
    });
  }

  _getKeyFromItemData(itemData) {
    return itemData[this.config.keyAttribute];
  }

  _onSelectItemSignal(receivedSignalInfo) {
    this.logger.debug('Received:', receivedSignalInfo.toString());
    const itemData = receivedSignalInfo.data;
    this.setState({
      addSelectedItem: itemData
    });
  }

  _onDeSelectItemSignal(receivedSignalInfo) {
    this.logger.debug('Received:', receivedSignalInfo.toString());
    const itemData = receivedSignalInfo.data;
    this.setState({
      removeSelectedItem: itemData
    });
  }

  _cancelBlurTimer() {
    if(this._blurTimeoutId != undefined) {
      window.clearTimeout(this._blurTimeoutId);
    }
  }

  _startBlurTimer(callback) {
    this._blurTimeoutId = window.setTimeout(
      callback,
      200);
  }

  _onFocusSignal(receivedSignalInfo) {
    this._cancelBlurTimer();
    this.setState({
      focus: true
    });
  }

  _onBlurSignal(receivedSignalInfo) {
    this._startBlurTimer(() => {
      this.setState({
        focus: false
      });
      this._sendLostFocusSignal();
    });
  }

  _onLoadMoreSignal(receivedSignalInfo) {
    if(this._isLoadingDataList) {
      this.logger.warning('Requested LoadMore while loading data.');
      return;
    }
    if(!this.state.data.next) {
      this.logger.warning('Requested LoadMore with no next page.');
      return;
    }
    this._requestDataListAndRefresh(this.makeRequestDataListOptions({
      next: true
    }), 'appendData');
  }

  _onLoadNextPageSignal(receivedSignalInfo) {
    if(!this.state.data.next) {
      this.logger.warning('Requested LoadNextPage with no next page.');
      return;
    }
    this._requestDataListAndRefresh(this.makeRequestDataListOptions({
      next: true
    }), 'data');
  }

  _onLoadPreviousPageSignal(receivedSignalInfo) {
    if(!this.state.data.previous) {
      this.logger.warning('Requested LoadPreviousPage with no previous page.');
      return;
    }
    this._requestDataListAndRefresh(this.makeRequestDataListOptions({
      previous: true
    }), 'data');
  }

  requestItemData(key) {
    throw new Error('requestItemData must be implemented in subclasses of AbstractDataListWidget.');
  }

  requestDataList() {
    throw new Error('requestDataList must be implemented in subclasses of AbstractDataListWidget.');
  }

  _cancelLoadingStateTimer() {
    if(this._loadingStateTimeoutId != undefined) {
      window.clearTimeout(this._loadingStateTimeoutId);
    }
  }

  _startLoadingStateTimer() {
    this._loadingStateTimeoutId = window.setTimeout(() => {
      this.setState({
        loading: true
      });
    }, 200);
  }

  _setLoadingState() {
    this._startLoadingStateTimer();
  }
  _unsetLoadingState() {
    this._cancelLoadingStateTimer();
    this.setState({
      loading: false
    });
  }

  _requestDataList(options) {
    return new Promise((resolve, reject) => {
      const dataRequestId = ++this._dataRequestId;
      // this.logger.debug(`Requesting data ${dataRequestId}`, options);
      this._isLoadingDataList = true;
      this._setLoadingState();
      this.requestDataList(options)
        .then((data) => {
          this._isLoadingDataList = false;
          this._unsetLoadingState();
          if(this._dataRequestId == dataRequestId) {
            resolve(data);
          } else {
            reject(new CancelledDataRequest(
              `Data list request ${dataRequestId} was cancelled.`, {
                options: options
              }));
          }
        })
        .catch((error) => {
          this._isLoadingDataList = false;
          this._unsetLoadingState();
          reject(error);
        });
    });
  }

  makeRequestDataListOptions(overrideOptions={}) {
    return Object.assign({}, {
      filtersMap: this.state.filtersMap,
      searchString: this.state.searchString
    }, overrideOptions);
  }

  _requestDataListAndRefresh(options, stateChangeAttribute='data') {
    this._requestDataList(options)
      .then((data) => {
        const stateChange = {};
        stateChange[stateChangeAttribute] = data;
        this.setState(stateChange);
      })
      .catch((error) => {
        if(error instanceof CancelledDataRequest) {
          this.logger.warning(error.toString(), error.options);
        } else {
          throw error;
        }
      });
  }
}