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';
}
}