Core_ViewModel_DocumentProvider.js

import { DocumentService } from '../Model/DocumentService';
import { Document } from '../Model/Document';
import { DocumentFilter } from './DocumentFilter';

import { EventDispatcher } from 'three';

/**
 * Represents the set of documents that is displayed in the view. This includes
 * the list of filtered documents, as well as the currently displayed one. It
 * uses a `DocumentService` to retrieve documents from the server. It also emits
 * events when the filtered documents, or the currently displayed document
 * change.
 */
export class DocumentProvider extends EventDispatcher {
  /**
   * Constructs a new documents provider.
   *
   * @param {DocumentService} service The document service.
   */
  constructor(service) {
    super();

    /**
     * The document service.
     *
     * @type {DocumentService}
     */
    this.service = service;

    /**
     * The list of filters.
     *
     * @type {Array<DocumentFilter>}
     */
    this.filters = [];

    /**
     * The list of all documents.
     *
     * @type {Array<Document>}
     */
    this.allDocuments = [];

    /**
     * The list of filtered documents.
     *
     * @type {Array<Document>}
     */
    this.filteredDocuments = [];

    /**
     * The currently displayed document.
     *
     * @type {number}
     */
    this.displayedDocumentIndex = undefined;
  }

  /**
   * Updates the filtered documents list by fetching them from the
   * `DocumentService` and applying the successive filters. Triggers the
   * `DOCUMENT_LIST_UPDATED` and then the `DISPLAYED_DOCUMENT_CHANGED` events.
   */
  async refreshDocumentList() {
    const previousDocument = this.getDisplayedDocument();
    this.allDocuments = await this.service
      .fetchDocuments()
      .catch((error) => console.log(error));

    if (!this.allDocuments) return;

    this.filteredDocuments = this.allDocuments.slice();

    for (const filter of this.filters) {
      this.filteredDocuments = filter.apply(this.filteredDocuments);
    }

    if (this.filteredDocuments.length > 0) {
      if (previousDocument) {
        const previousDisplayedId = previousDocument.id;
        const newIndex = this.filteredDocuments.findIndex(
          (doc) => doc.id === previousDisplayedId
        );
        this.displayedDocumentIndex = newIndex >= 0 ? newIndex : 0;
      } else {
        this.displayedDocumentIndex = 0;
      }
    } else {
      this.displayedDocumentIndex = undefined;
    }
    this.dispatchEvent({
      type: DocumentProvider.EVENT_FILTERED_DOCS_UPDATED,
      documents: this.getFilteredDocuments(),
    });
    this.dispatchEvent({
      type: DocumentProvider.EVENT_DISPLAYED_DOC_CHANGED,
      document: this.getDisplayedDocument(),
    });
  }

  /**
   * Adds a filter to the filtering pipeline.
   *
   * @param {DocumentFilter} newFilter The new filter to add.
   */
  addFilter(newFilter) {
    if (!(newFilter instanceof DocumentFilter)) {
      throw 'addFilter() expects a DocumentFilter parameter';
    }
    this.filters.push(newFilter);
  }

  /**
   * Sets the given document as the displayed one.
   *
   * @param {Document} doc The document.
   */
  setDisplayedDocument(doc) {
    const index = this.filteredDocuments.findIndex(
      (filteredDoc) => doc.id === filteredDoc.id
    );

    if (index < 0) {
      throw 'Document not found.';
    }

    this.setDisplayedDocumentIndex(index);
  }

  /**
   * Change the displayed document index. Sends a `DISPLAYED_DOCUMENT_CHANGED`
   * event.
   *
   * @param {number} index The new document index.
   */
  setDisplayedDocumentIndex(index) {
    if (this.displayedDocumentIndex === undefined) {
      console.warn(
        'Cannot change displayed document if no document is present'
      );
      return;
    }

    if (index < 0 || index >= this.filteredDocuments.length) {
      throw 'Document index out of bounds : ' + index;
    }

    this.displayedDocumentIndex = index;
    this.dispatchEvent({
      type: DocumentProvider.EVENT_DISPLAYED_DOC_CHANGED,
      document: this.getDisplayedDocument(),
    });
  }

  /**
   * Shift the displayed document index.  The filtered array is treated as
   * cyclical. Sends a `DISPLAYED_DOCUMENT_CHANGED` event.
   *
   * @param {number} offset The offset that will be applied to the current
   * index.
   */
  shiftDisplayedDocumentIndex(offset) {
    if (this.displayedDocumentIndex === undefined) {
      console.warn(
        'Cannot change displayed document if no document is present'
      );
      return;
    }

    offset = offset % this.filteredDocuments.length;
    this.displayedDocumentIndex =
      (this.filteredDocuments.length + this.displayedDocumentIndex + offset) %
      this.filteredDocuments.length;

    this.dispatchEvent({
      type: DocumentProvider.EVENT_DISPLAYED_DOC_CHANGED,
      document: this.getDisplayedDocument(),
    });
  }

  /**
   * Returns the list of all documents.
   *
   * @returns {Array<Document>} An array with all documents
   */
  getAllDocuments() {
    return this.allDocuments;
  }

  /**
   * Returns the filtered list of documents.
   *
   * @returns {Array<Document>} An array with filtered documents
   */
  getFilteredDocuments() {
    return this.filteredDocuments;
  }

  /**
   * Returns the currently displayed document.
   *
   * @returns {Document | undefined} The displayed document
   */
  getDisplayedDocument() {
    if (this.displayedDocumentIndex === undefined) {
      return undefined;
    }

    return this.filteredDocuments[this.displayedDocumentIndex];
  }

  /**
   * Returns the displayed document index.
   *
   * @returns {number | undefined} The index of the displayed document
   */
  getDisplayedDocumentIndex() {
    return this.displayedDocumentIndex;
  }

  /**
   * Returns the image corresponding to the displayed document. It is a string
   * that can be put into the `src` attribute of an `img` tag (so either an
   * URL or a base64 encoded file).
   *
   * @async
   * @returns {Promise<string | undefined>} A promise for the document image
   */
  async getDisplayedDocumentImage() {
    if (this.displayedDocumentIndex === undefined) {
      return undefined;
    }

    return await this.service.fetchDocumentImage(this.getDisplayedDocument());
  }

  // //////////
  // /// EVENTS

  static get EVENT_FILTERED_DOCS_UPDATED() {
    return 'EVENT_FILTERED_DOCS_UPDATED';
  }

  static get EVENT_DISPLAYED_DOC_CHANGED() {
    return 'EVENT_DISPLAYED_DOC_CHANGED';
  }
}