UNPKG

coveo-search-ui

Version:

Coveo JavaScript Search Framework

351 lines (314 loc) 12.2 kB
import { $$ } from '../utils/Dom'; import { Assert } from '../misc/Assert'; import { Utils } from '../utils/Utils'; import { BaseComponent } from '../ui/Base/BaseComponent'; import * as _ from 'underscore'; import { IStringMap } from '../rest/GenericParam'; export const MODEL_EVENTS = { PREPROCESS: 'preprocess', CHANGE_ONE: 'change:', CHANGE: 'change', RESET: 'reset', ALL: 'all' }; export interface IModelSetOptions { silent?: boolean; customAttribute?: boolean; validateType?: boolean; } export interface IAttributeChangedEventArg { attribute: string; value: any; } export interface IAttributesChangedEventArg { attributes: {}; } export interface IModelChangedEventArg { model: Model; } /** * A *model* is a key-value store that triggers various JavaScript events when any value associated to one of its key changes.<br/> * This class is meant to be extended, one of the most important extension being the {@link QueryStateModel} class.<br/> * Components set values in this key-value store and listen to triggered events in order to update themselves accordingly.<br/> */ export class Model extends BaseComponent { /** * The attributes contained in this model.</br> * Normally, you should not set attributes directly on this property, as this would prevent required events from being triggered. */ public attributes: IStringMap<any>; public defaultAttributes: IStringMap<any>; private eventNameSpace; /** * The event types that can be triggered:<br/> * • `preprocess`: triggered before a value is set on an attribute. This allows the value to be modified before it is set.<br/> * • `changeOne`: triggered when a single value changes.</br> * • `change`: triggered when one or many values change.</br> * • `reset`: triggered when all attributes are reset to their default values. </br> * • `all`: triggered after the `change` event.</br> * @type {{preprocess: string, changeOne: string, change: string, reset: string, all: string}} */ public static eventTypes = { preprocess: 'preprocess', changeOne: 'change:', change: 'change', reset: 'reset', all: 'all' }; constructor(element: HTMLElement, id: string, attributes: { [key: string]: any }) { super(element, id); this.eventNameSpace = id; this.defaultAttributes = Utils.extendDeep(this.defaultAttributes, attributes); this.attributes = attributes; this.logger.debug('Creating model'); } /** * Sets the value of a single specific attribute.</br> * Note: this method calls the `setMultiple` method. * @param attribute * the specific attribute whose value is to be set. * @param value * the value to set the attribute to. * @param options * the options (see {@link setMultiple}). */ public set(attribute: string, value: any, options?: IModelSetOptions) { const toSet: { [key: string]: any } = {}; toSet[attribute] = value; this.setMultiple(toSet, options); } /** * Gets an object containing all *active* registered attribute key-values.</br> * An attribute is considered active when its value is not in its default state. * @returns {{object}} */ public getAttributes() { const attributes: { [key: string]: any } = {}; _.each(this.attributes, (attribute, key) => { if (_.isObject(attribute)) { if (!Utils.objectEqual(attribute, this.defaultAttributes[key])) { attributes[key] = Utils.extendDeep({}, attribute); } } else if (attribute != this.defaultAttributes[key]) { attributes[key] = attribute; } }); return attributes; } /** * Sets the values of one or many attributes.</br> * This method may trigger the following events (in order):</br> * • `preprocess`</br> * • `changeOne`</br> * • `change`</br> * • `all` * @param toSet * the key-value list of attributes with their new intended values. * @param options * if the `customAttribute` option is set to `true`, the method will not validate whether an attribute is registered or not.</br> * If the `validateType` option is set to `true`, the method will ensure that each value type is correct.</br> * If the `silent` option is set to `true`, then the `changeOne`, `change` and `all` events will not be triggered. */ public setMultiple(toSet: { [key: string]: any }, options?: IModelSetOptions) { let anythingChanged = false; this.preprocessEvent(toSet); _.each(<_.Dictionary<any>>toSet, (value, attribute?) => { if (!options || !options.customAttribute) { this.checkIfAttributeExists(attribute); } value = this.parseToCorrectType(attribute, value); if (!options || options.validateType) { if (!this.typeIsValid(attribute, value)) { return; } } if (this.checkIfAttributeChanged(attribute, value)) { this.attributes[attribute] = value; anythingChanged = true; if (options == null || !options.silent) { this.attributeHasChangedEvent(attribute); } } }); if (anythingChanged && (options == null || !options.silent)) { this.attributesHasChangedEvent(); this.anyEvent(); } } /** * Sets a new default value to a single specific attribute.</br> * Note: specifying a new attribute default value does not set the attribute to that value. This can be done using the {@link setDefault} method. * @param attribute * the specific attribute whose default value is to be changed. * @param value * the new intended default value. * @param options * if the `customAttribute` option is set to `true`, the method will not validate whether the attribute is registered or not. */ public setNewDefault(attribute: string, value: any, options?: IModelSetOptions) { if (!options || !options.customAttribute) { this.checkIfAttributeExists(attribute); } this.defaultAttributes[attribute] = value; } /** * Sets a single specific attribute to its default value.</br> * Note: this method calls the {@link setMultiple} method without specifying any option. * @param attribute * the specific attribute whose value is to be set to its default value. */ public setDefault(attribute: string) { this.set(attribute, this.defaultAttributes[attribute]); } /** * Gets the value of a single specific attribute.</br> * If no attribute is specified, the method instead returns an object containing all registered attribute key-values. * @param attribute * the specific attribute whose value should be returned. * @returns {any} */ public get(attribute?: string): any { if (attribute == undefined) { return this.attributes; } else { return this.attributes[attribute]; } } /** * Gets the default value of a single specific attribute.</br> * If no attribute is specified, the method instead returns an object containing all registered attribute key-default values. * @param attribute * the specific attribute whose default value should be returned. * @returns {any} */ public getDefault(attribute?: string): any { if (attribute == undefined) { return this.defaultAttributes; } else { return this.defaultAttributes[attribute]; } } /** * Resets each registered attribute to its default value.</br> * Note: this method calls the {@link setMultiple} method without specifying any options.</br> * After the `setMultiple` call has returned, this method triggers the `reset` event. */ public reset() { this.setMultiple(this.defaultAttributes); this.modelWasResetEvent(); } /** * Registers a new attribute key-value. * @param attribute * the name of the new attribute to register. * @param defaultValue * the newly registered attribute default value. */ public registerNewAttribute(attribute: string, defaultValue: any) { this.defaultAttributes[attribute] = defaultValue; this.attributes[attribute] = defaultValue; } /** * Gets a string displaying the event namespace followed by the specific event name. The returned string is formatted thus:</br> * `[eventNameSpace]:[eventName]` * @example `getEventName("reset");` could return `"state:reset"`. * @param event * the event name. * @returns {string} */ public getEventName(event: string) { return this.eventNameSpace + ':' + event; } private attributesHasChangedEvent() { $$(this.element).trigger(this.getEventName(Model.eventTypes.change), this.createAttributesChangedArgument()); } private attributeHasChangedEvent(attr: string) { $$(this.element).trigger(this.getEventName(Model.eventTypes.changeOne) + attr, this.createAttributeChangedArgument(attr)); } private preprocessEvent(attributes: { [key: string]: any }) { $$(this.element).trigger(this.getEventName(Model.eventTypes.preprocess), attributes); } private modelWasResetEvent() { $$(this.element).trigger(this.getEventName(Model.eventTypes.reset), this.createModelChangedArgument()); } private anyEvent() { $$(this.element).trigger(this.getEventName(Model.eventTypes.all), this.createModelChangedArgument()); } private createAttributeChangedArgument(attribute: string): IAttributeChangedEventArg { return { attribute: attribute, value: this.attributes[attribute] }; } private createAttributesChangedArgument(): IAttributesChangedEventArg { return { attributes: this.attributes }; } private createModelChangedArgument(): IModelChangedEventArg { return { model: this }; } private checkIfAttributeExists(attribute: string) { Assert.check(_.has(this.attributes, attribute), `The attribute ${attribute} is not registered.`); } private typeIsValid(attribute: string, value: any): boolean { if (!Utils.isNullOrUndefined(this.attributes[attribute]) && !Utils.isUndefined(value)) { if (_.isNumber(this.attributes[attribute])) { return this.validateNumber(attribute, value); } else if (_.isBoolean(this.attributes[attribute])) { return this.validateBoolean(attribute, value); } else { return this.validateOther(attribute, value); } } return true; } private validateNumber(attribute: string, value: any): boolean { if (!_.isNumber(value) || isNaN(value)) { this.logger.error(`Non-matching type for ${attribute}. Expected number and got ${value}`); return false; } return true; } private validateBoolean(attribute: string, value: any) { if (!_.isBoolean(value) && !Utils.parseBooleanIfNotUndefined(value) !== undefined) { this.logger.error(`Non matching type for ${attribute}. Expected boolean and got ${value}`); return false; } return true; } private validateOther(attribute: string, value: any) { if (!Utils.isNullOrUndefined(this.defaultAttributes[attribute])) { const valueType = typeof value; const attributeType = typeof this.defaultAttributes[attribute]; if (valueType !== attributeType) { this.logger.error(`Non-matching type for ${attribute}. Expected ${attributeType} and got ${valueType}`); return false; } } return true; } private parseToCorrectType(attribute: string, value: any): any { if (_.isNumber(this.attributes[attribute])) { return parseInt(value, 10); } else if (_.isBoolean(this.attributes[attribute])) { if (_.isBoolean(value)) { return value; } else { return Utils.parseBooleanIfNotUndefined(value); } } return value; } private checkIfAttributeChanged(attribute: string, newValue: any): boolean { let oldValue = this.attributes[attribute]; if (_.isNumber(oldValue) || _.isString(oldValue) || _.isBoolean(oldValue)) { return oldValue !== newValue; } if (_.isArray(oldValue)) { return !Utils.arrayEqual(oldValue, newValue); } if (_.isObject(oldValue)) { return !Utils.objectEqual(oldValue, newValue); } return true; } public debugInfo() { return null; } }