coveo-search-ui
Version:
Coveo JavaScript Search Framework
457 lines (407 loc) • 13.6 kB
text/typescript
import { IQueryResult } from '../rest/QueryResult';
import * as _ from 'underscore';
import { IStringMap } from '../rest/GenericParam';
const isCoveoFieldRegex = /^@[a-zA-Z0-9_\.]+$/;
export class Utils {
static isUndefined(obj: any): boolean {
return typeof obj == 'undefined';
}
static isNull(obj: any): boolean {
return obj === null;
}
static isNullOrUndefined(obj: any): boolean {
return Utils.isUndefined(obj) || Utils.isNull(obj);
}
static exists(obj: any): boolean {
return !Utils.isNullOrUndefined(obj);
}
static toNotNullString(str: string): string {
return _.isString(str) ? str : '';
}
static anyTypeToString(value: any): string {
return value ? value.toString() : '';
}
static isNullOrEmptyString(str: string): boolean {
return Utils.isNullOrUndefined(str) || !Utils.isNonEmptyString(str);
}
static isNonEmptyString(str: string): boolean {
return _.isString(str) && str !== '';
}
static isEmptyString(str: string): boolean {
return !Utils.isNonEmptyString(str);
}
static stringStartsWith(str: string, startWith: string): boolean {
return str.slice(0, startWith.length) == startWith;
}
static stringEndsWith(str: string, endsWith: string): boolean {
return str.substring(str.length - endsWith.length, str.length) === endsWith;
}
static isNonEmptyArray(obj: any): boolean {
return _.isArray(obj) && obj.length > 0;
}
static isEmptyArray(obj: any): boolean {
return !Utils.isNonEmptyArray(obj);
}
static isHtmlElement(obj: any): obj is HTMLElement {
if (window['HTMLElement'] != undefined) {
return obj instanceof HTMLElement;
} else {
// IE 8 FIX
return obj && obj.nodeType && obj.nodeType == 1;
}
}
static parseIntIfNotUndefined(str: string): number {
if (Utils.isNonEmptyString(str)) {
return parseInt(str, 10);
} else {
return undefined;
}
}
static parseFloatIfNotUndefined(str: string): number {
let a: any = 't';
a instanceof HTMLDocument;
if (Utils.isNonEmptyString(str)) {
return parseFloat(str);
} else {
return undefined;
}
}
static round(num: number, decimals: number) {
return Math.round(num * Math.pow(10, decimals)) / Math.pow(10, decimals);
}
static parseBooleanIfNotUndefined(str: string): boolean {
if (Utils.isNonEmptyString(str)) {
switch (str.toLowerCase()) {
case 'true':
case '1':
case 'yes':
return true;
case 'false':
case '0':
case 'no':
return false;
default:
return undefined;
}
} else {
return undefined;
}
}
static trim(value: string): string {
if (value == null) {
return null;
}
return value.replace(/^\s+|\s+$/g, '');
}
static encodeHTMLEntities(rawStr: string) {
let ret = [];
for (let i = rawStr.length - 1; i >= 0; i--) {
if (/^[a-z0-9]/i.test(rawStr[i])) {
ret.unshift(rawStr[i]);
} else {
ret.unshift(['&#', rawStr.charCodeAt(i), ';'].join(''));
}
}
return ret.join('');
}
static decodeHTMLEntities(rawString: string) {
return rawString.replace(/&#(\d+);/g, function (match, dec) {
return String.fromCharCode(dec);
});
}
static safeEncodeURIComponent(rawString: string) {
// yiiip...
// Explanation : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
// Solution : https://stackoverflow.com/a/17109094
// Basically some unicode character (low-high surrogate) will throw an "invalid malformed URI" error when being encoded as an URI component, and the pair of character is incomplete.
// This simply removes those pesky characters
if (_.isString(rawString)) {
return encodeURIComponent(
rawString
.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/g, '')
.split('')
.reverse()
.join('')
.replace(/[\uDC00-\uDFFF](?![\uD800-\uDBFF])/g, '')
.split('')
.reverse()
.join('')
);
} else {
// If the passed value is not a string, we probably don't want to do anything here...
// The base browser function should be resilient enough
return encodeURIComponent(rawString);
}
}
static arrayEqual(array1: any[], array2: any[], sameOrder: boolean = true): boolean {
if (sameOrder) {
return _.isEqual(array1, array2);
} else {
let arrays = [array1, array2];
return _.all(arrays, (array: any[]) => {
return array.length == arrays[0].length && _.difference(array, arrays[0]).length == 0;
});
}
}
static objectEqual(obj1: Object, obj2: Object): boolean {
return _.isEqual(obj1, obj2);
}
static isCoveoField(field: string): boolean {
return isCoveoFieldRegex.test(field);
}
static escapeRegexCharacter(str: string) {
let ret = str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
return ret;
}
static getCaseInsensitiveProperty(object: {}, name: string): any {
// First try using a fast case-sensitive lookup
let value = object[name];
// Then try a fast case-sensitive lookup with lowercase name
if (value == null) {
let lowerCaseName = name.toLowerCase();
value = object[lowerCaseName];
// Then try a slow scanning of all the properties
if (value == null) {
let matchingKey = _.find(_.keys(object), (key: string) => key.toLowerCase() == lowerCaseName);
if (matchingKey != null) {
value = object[matchingKey];
}
}
}
return value;
}
/**
* Get the value of the first field from the array and defined in the result.
*
* @param result a QueryResult in which to ge the fieldvalue.
* @param name One or multiple fieldNames to get the value.
*/
static getFirstAvailableFieldValue(result: IQueryResult, fieldNames: Array<string>): string | undefined {
for (let i = 0; i < fieldNames.length; i++) {
let value = Utils.getFieldValue(result, fieldNames[i]);
if (value !== undefined) {
return value;
}
}
return undefined;
}
static getFieldValue(result: IQueryResult, name: string): any {
// Be as forgiving as possible about the field name, since we expect
// user provided values.
if (name == null) {
return undefined;
}
name = Utils.trim(name);
if (name[0] == '@') {
name = name.substr(1);
}
if (name == '') {
return undefined;
}
// At this point the name should be well formed
if (!Utils.isCoveoField('@' + name)) {
throw `Not a valid field : ${name}`;
}
// Handle namespace field values of the form sf.Foo.Bar
let parts = name.split('.').reverse();
let obj = result.raw;
while (parts.length > 1) {
obj = Utils.getCaseInsensitiveProperty(obj, parts.pop());
if (Utils.isUndefined(obj)) {
return undefined;
}
}
let value = Utils.getCaseInsensitiveProperty(obj, parts[0]);
// If still nothing, look at the root of the result
if (value == null) {
value = Utils.getCaseInsensitiveProperty(result, name);
}
return value;
}
static throttle(func, wait, options: { leading?: boolean; trailing?: boolean } = {}, context?, args?) {
let result;
let timeout: number = null;
let previous = 0;
let later = function () {
previous = options.leading === false ? 0 : new Date().getTime();
timeout = null;
result = func.apply(context, args);
};
return function () {
let now = new Date().getTime();
if (!previous && options.leading === false) {
previous = now;
}
let remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0) {
clearTimeout(timeout);
timeout = null;
previous = now;
result = func.apply(context, args);
} else if (!timeout && options.trailing !== false) {
timeout = window.setTimeout(later, remaining);
}
return result;
};
}
static extendDeep(target, src): {} {
if (!target) {
target = {};
}
let isArray = _.isArray(src);
let toReturn = (isArray && []) || {};
if (isArray) {
target = target || [];
toReturn = toReturn['concat'](target);
_.each(src, (e, i, obj) => {
if (typeof target[i] === 'undefined') {
toReturn[i] = <any>e;
} else if (typeof e === 'object' && !_.isElement(e)) {
toReturn[i] = Utils.extendDeep(target[i], e);
} else {
if (target.indexOf(e) === -1) {
toReturn['push'](e);
}
}
});
} else {
if (target && typeof target === 'object' && !_.isElement(target)) {
_.each(_.keys(target), key => {
toReturn[key] = target[key];
});
}
_.each(_.keys(src), key => {
if (typeof src[key] !== 'object' || !src[key]) {
toReturn[key] = src[key];
} else {
if (!target[key]) {
toReturn[key] = src[key];
} else {
toReturn[key] = Utils.extendDeep(target[key], src[key]);
}
}
});
}
return toReturn;
}
static getQueryStringValue(key, queryString = window.location.search) {
return queryString.replace(new RegExp('^(?:.*[&\\?]' + key.replace(/[\.\+\*]/g, '\\$&') + '(?:\\=([^&]*))?)?.*$', 'i'), '$1');
}
static isValidUrl(str: string): boolean {
let regexp = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/;
return regexp.test(str);
}
static debounce(func: Function, wait: number) {
let timeout: number;
let stackTraceTimeout: number;
return function (...args: any[]) {
if (timeout == null) {
timeout = window.setTimeout(() => {
timeout = null;
}, wait);
stackTraceTimeout = setTimeout(() => {
func.apply(this, args);
stackTraceTimeout = null;
});
} else if (stackTraceTimeout == null) {
clearTimeout(timeout);
timeout = window.setTimeout(() => {
func.apply(this, args);
timeout = null;
}, wait);
}
};
}
static readCookie(name: string) {
let nameEQ = name + '=';
let ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1, c.length);
}
if (c.indexOf(nameEQ) == 0) {
return c.substring(nameEQ.length, c.length);
}
}
return null;
}
static toDashCase(camelCased: string) {
return camelCased.replace(/([a-z][A-Z])/g, g => g[0] + '-' + g[1].toLowerCase());
}
static toCamelCase(dashCased: string) {
return dashCased.replace(/-([a-z])/g, g => g[1].toUpperCase());
}
// Based on http://stackoverflow.com/a/8412989
static parseXml(xml: string): XMLDocument {
if (typeof DOMParser != 'undefined') {
return new DOMParser().parseFromString(xml, 'text/xml');
} else if (typeof ActiveXObject != 'undefined' && new ActiveXObject('Microsoft.XMLDOM')) {
var xmlDoc = new ActiveXObject('Microsoft.XMLDOM');
xmlDoc.async = 'false';
xmlDoc.loadXML(xml);
return xmlDoc;
} else {
throw new Error('No XML parser found');
}
}
static copyObject<T>(target: T, src: T) {
_.each(_.keys(src), key => {
if (typeof src[key] !== 'object' || !src[key]) {
target[key] = src[key];
} else if (!target[key]) {
target[key] = src[key];
} else {
this.copyObject(target[key], src[key]);
}
});
}
static copyObjectAttributes<T>(target: T, src: T, attributes: string[]) {
_.each(_.keys(src), key => {
if (_.contains(attributes, key)) {
if (typeof src[key] !== 'object' || !src[key]) {
target[key] = src[key];
} else if (!target[key]) {
target[key] = src[key];
} else {
this.copyObject(target[key], src[key]);
}
}
});
}
static concatWithoutDuplicate(firstArray: any[], secondArray: any[]) {
let diff = _.difference(secondArray, firstArray);
if (diff && diff.length > 0) {
firstArray = firstArray.concat(diff);
}
return firstArray;
}
static differenceBetweenObjects<T>(firstObject: IStringMap<T>, secondObject: IStringMap<T>) {
const difference: IStringMap<T> = {};
const addDiff = (first: IStringMap<T>, second: IStringMap<T>) => {
for (const key in first) {
if (first[key] !== second[key] && difference[key] == null) {
difference[key] = first[key];
}
}
};
addDiff(firstObject, secondObject);
addDiff(secondObject, firstObject);
return difference;
}
static resolveAfter<T = void>(ms: number, returns?: T): Promise<T> {
return new Promise(resolve => setTimeout(() => (returns !== undefined ? resolve(returns) : resolve()), ms));
}
static reorderValuesByKeys<T, K extends string>(values: T[], order: K[], getKey: (value: T) => K) {
const valuesMap = values.reduce<{ [key: string]: T }>((map, value) => ({ ...map, [getKey(value)]: value }), {});
const orderedValues: T[] = [];
order.forEach(keyToAppend => {
if (valuesMap[keyToAppend]) {
orderedValues.push(valuesMap[keyToAppend]);
}
});
return [...orderedValues, ..._.without(values, ...orderedValues)];
}
}