1139 lines
42 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { Platform } from 'ionic-angular';
import { Observable } from 'rxjs';
import { InAppBrowser, InAppBrowserObject } from '@ionic-native/in-app-browser';
import { Clipboard } from '@ionic-native/clipboard';
import { CoreAppProvider } from '../app';
import { CoreDomUtilsProvider } from './dom';
import { CoreLoggerProvider } from '../logger';
import { TranslateService } from '@ngx-translate/core';
import { CoreLangProvider } from '../lang';
export interface PromiseDefer {
promise?: Promise<any>; // Promise created.
resolve?: (value?: any) => any; // Function to resolve the promise.
reject?: (reason?: any) => any; // Function to reject the promise.
}
/*
* "Utils" service with helper functions.
*/
@Injectable()
export class CoreUtilsProvider {
logger;
iabInstance: InAppBrowserObject;
constructor(private iab: InAppBrowser, private appProvider: CoreAppProvider, private clipboard: Clipboard,
private domUtils: CoreDomUtilsProvider, logger: CoreLoggerProvider, private translate: TranslateService,
private platform: Platform, private langProvider: CoreLangProvider) {
this.logger = logger.getInstance('CoreUtilsProvider');
}
/**
* Similar to Promise.all, but if a promise fails this function's promise won't be rejected until ALL promises have finished.
*
* @param {Promise<any>[]} promises Promises.
* @return {Promise<any>} Promise resolved if all promises are resolved and rejected if at least 1 promise fails.
*/
allPromises(promises: Promise<any>[]): Promise<any> {
if (!promises || !promises.length) {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
let count = 0,
total = promises.length,
error;
promises.forEach((promise) => {
promise.catch((err) => {
error = err;
}).finally(() => {
count++;
if (count === total) {
// All promises have finished, reject/resolve.
if (error) {
reject(error);
} else {
resolve();
}
}
});
});
});
}
/**
* Compare two objects. This function won't compare functions and proto properties, it's a basic compare.
* Also, this will only check if itemA's properties are in itemB with same value. This function will still
* return true if itemB has more properties than itemA.
*
* @param {any} itemA First object.
* @param {any} itemB Second object.
* @param {number} [maxLevels=0] Number of levels to reach if 2 objects are compared.
* @param {number} [level=0] Current deep level (when comparing objects).
* @param {boolean} [undefinedIsNull=true] True if undefined is equal to null. Defaults to true.
* @return {boolean} Whether both items are equal.
*/
basicLeftCompare(itemA: any, itemB: any, maxLevels = 0, level = 0, undefinedIsNull = true) : boolean {
if (typeof itemA == 'function' || typeof itemB == 'function') {
return true; // Don't compare functions.
} else if (typeof itemA == 'object' && typeof itemB == 'object') {
if (level >= maxLevels) {
return true; // Max deep reached.
}
let equal = true;
for (let name in itemA) {
let value = itemA[name];
if (name == '$$hashKey') {
// Ignore $$hashKey property since it's a "calculated" property.
return;
}
if (!this.basicLeftCompare(value, itemB[name], maxLevels, level + 1)) {
equal = false;
}
}
return equal;
} else {
if (undefinedIsNull && (
(typeof itemA == 'undefined' && itemB === null) || (itemA === null && typeof itemB == 'undefined'))) {
return true;
}
// We'll treat "2" and 2 as the same value.
let floatA = parseFloat(itemA),
floatB = parseFloat(itemB);
if (!isNaN(floatA) && !isNaN(floatB)) {
return floatA == floatB;
}
return itemA === itemB;
}
}
/**
* Blocks leaving a view. This function should be used in views that want to perform a certain action before
* leaving (usually, ask the user if he wants to leave because some data isn't saved).
*
* @param {Object} scope View's scope.
* @param {Function} canLeaveFn Function called when the user wants to leave the view. Must return a promise
* resolved if the view should be left, rejected if the user should stay in the view.
* @param {Object} [currentView] Current view. Defaults to $ionicHistory.currentView().
* @return {Object} Object with:
* -back: Original back function.
* -unblock: Function to unblock. It is called automatically when scope is destroyed.
*/
blockLeaveView = function(scope, canLeaveFn, currentView) {
// @todo
// currentView = currentView || $ionicHistory.currentView();
// var unregisterHardwareBack,
// leaving = false,
// hasSplitView = $ionicPlatform.isTablet() && $state.current.name.split('.').length == 3,
// skipSplitViewLeave = false;
// // Override Ionic's back button behavior.
// $rootScope.$ionicGoBack = goBack;
// // Override Android's back button. We set a priority of 101 to override the "Return to previous view" action.
// unregisterHardwareBack = $ionicPlatform.registerBackButtonAction(goBack, 101);
// // Add function to the stack.
// backFunctionsStack.push(goBack);
// if (hasSplitView) {
// // Block split view.
// blockSplitView(true);
// }
// scope.$on('$destroy', unblock);
// return {
// back: originalBackFunction,
// unblock: unblock
// };
// // Function called when the user wants to leave the view.
// function goBack() {
// // Check that we're leaving the current view, since the user can navigate to other views from here.
// if ($ionicHistory.currentView() !== currentView) {
// // It's another view.
// originalBackFunction();
// return;
// }
// if (leaving) {
// // Leave view pending, don't call again.
// return;
// }
// leaving = true;
// canLeaveFn().then(function() {
// // User confirmed to leave or there was no need to confirm, go back.
// // Skip next leave view from split view if there's one since we already checked if user can leave.
// skipSplitViewLeave = hasSplitView;
// originalBackFunction();
// }).finally(function() {
// leaving = false;
// });
// }
// // Leaving current view when it's in split view.
// function leaveViewInSplitView() {
// if (skipSplitViewLeave) {
// skipSplitViewLeave = false;
// return $q.when();
// }
// return canLeaveFn();
// }
// // Restore original back functions.
// function unblock() {
// unregisterHardwareBack();
// if (hasSplitView) {
// // Unblock split view.
// blockSplitView(false);
// }
// // Remove function from the stack.
// var position = backFunctionsStack.indexOf(goBack);
// if (position > -1) {
// backFunctionsStack.splice(position, 1);
// }
// // Revert go back only if it hasn't been overridden by another view.
// if ($rootScope.$ionicGoBack === goBack) {
// if (!backFunctionsStack.length) {
// // Shouldn't happen. Reset stack.
// backFunctionsStack = [originalBackFunction];
// $rootScope.$ionicGoBack = originalBackFunction;
// } else {
// $rootScope.$ionicGoBack = backFunctionsStack[backFunctionsStack.length - 1];
// }
// }
// }
// // Block or unblock split view.
// function blockSplitView(block) {
// $rootScope.$broadcast(mmCoreSplitViewBlock, {
// block: block,
// blockFunction: leaveViewInSplitView,
// state: currentView.stateName,
// stateParams: currentView.stateParams
// });
// }
}
/**
* Close the InAppBrowser window.
*
* @param {boolean} [closeAll] Desktop only. True to close all secondary windows, false to close only the "current" one.
*/
closeInAppBrowser(closeAll: boolean) : void {
if (this.iabInstance) {
this.iabInstance.close();
if (closeAll && this.appProvider.isDesktop()) {
require('electron').ipcRenderer.send('closeSecondaryWindows');
}
}
}
/**
* Copy properties from one object to another.
*
* @param {any} from Object to copy the properties from.
* @param {any} to Object where to store the properties.
*/
copyProperties(from: any, to: any) : void {
for (let name in from) {
let value = from[name];
if (typeof value == 'object') {
// Clone the object.
to[name] = Object.assign({}, value);
} else {
to[name] = value;
}
}
}
/**
* Copies a text to clipboard and shows a toast message.
*
* @param {string} text Text to be copied
* @return {Promise<any>} Promise resolved when text is copied.
*/
copyToClipboard(text: string) : Promise<any> {
return this.clipboard.copy(text).then(() => {
// Show toast using ionicLoading.
return this.domUtils.showToast('mm.core.copiedtoclipboard', true);
}).catch(() => {
// Ignore errors.
});
}
/**
* Empties an array without losing its reference.
*
* @param {any[]} array Array to empty.
*/
emptyArray(array: any[]) : void {
array.length = 0; // Empty array without losing its reference.
}
/**
* Removes all properties from an object without losing its reference.
*
* @param {object} object Object to remove the properties.
*/
emptyObject(object: object) : void {
for (let key in object) {
if (object.hasOwnProperty(key)) {
delete object[key];
}
}
}
/**
* Execute promises one depending on the previous.
*
* @param {any[]} orderedPromisesData Data to be executed including the following values:
* - func: Function to be executed.
* - context: Context to pass to the function. This allows using "this" inside the function.
* - params: Array of data to be sent to the function.
* - blocking: Boolean. If promise should block the following.
* @return {Promise<any>} Promise resolved when all promises are resolved.
*/
executeOrderedPromises(orderedPromisesData: any[]) : Promise<any> {
let promises = [],
dependency = Promise.resolve();
// Execute all the processes in order.
for (let i in orderedPromisesData) {
let data = orderedPromisesData[i],
promise;
// Add the process to the dependency stack.
promise = dependency.finally(() => {
let prom;
try {
prom = data.func.apply(data.context, data.params || []);
} catch (e) {
this.logger.error(e.message);
return;
}
return prom;
});
promises.push(promise);
// If the new process is blocking, we set it as the dependency.
if (data.blocking) {
dependency = promise;
}
}
// Return when all promises are done.
return this.allPromises(promises);
}
/**
* Flatten an object, moving subobjects' properties to the first level using dot notation. E.g.:
* {a: {b: 1, c: 2}, d: 3} -> {'a.b': 1, 'a.c': 2, d: 3}
*
* @param {object} obj Object to flatten.
* @return {object} Flatten object.
*/
flattenObject(obj: object) : object {
let toReturn = {};
for (let name in obj) {
if (!obj.hasOwnProperty(name)) continue;
if (typeof obj[name] == 'object') {
let flatObject = this.flattenObject(obj[name]);
for (let subName in flatObject) {
if (!flatObject.hasOwnProperty(subName)) continue;
toReturn[name + '.' + subName] = flatObject[subName];
}
} else {
toReturn[name] = obj[name];
}
}
return toReturn;
}
/**
* Given an array of strings, return only the ones that match a regular expression.
*
* @param {string[]} array Array to filter.
* @param {RegExp} regex RegExp to apply to each string.
* @return {string[]} Filtered array.
*/
filterByRegexp(array: string[], regex: RegExp) : string[] {
if (!array || !array.length) {
return [];
}
return array.filter((entry) => {
let matches = entry.match(regex);
return matches && matches.length;
});
}
/**
* Filter the list of site IDs based on a isEnabled function.
*
* @param {string[]} siteIds Site IDs to filter.
* @param {Function} isEnabledFn Function to call for each site. Must return true or a promise resolved with true if enabled.
* It receives a siteId param and all the params sent to this function after 'checkAll'.
* @param {boolean} [checkAll] True if it should check all the sites, false if it should check only 1 and treat them all
* depending on this result.
* @param {any} ...args All the params sent after checkAll will be passed to isEnabledFn.
* @return {Promise<string[]>} Promise resolved with the list of enabled sites.
*/
filterEnabledSites(siteIds: string[], isEnabledFn: Function, checkAll?: boolean, ...args) : Promise<string[]> {
let promises = [],
enabledSites = [];
for (let i in siteIds) {
let siteId = siteIds[i];
if (checkAll || !promises.length) {
promises.push(Promise.resolve(isEnabledFn.apply(isEnabledFn, [siteId].concat(args))).then((enabled) => {
if (enabled) {
enabledSites.push(siteId);
}
}));
}
}
return this.allPromises(promises).catch(() => {
// Ignore errors.
}).then(() => {
if (!checkAll) {
// Checking 1 was enough, so it will either return all the sites or none.
return enabledSites.length ? siteIds : [];
} else {
return enabledSites;
}
});
}
/**
* Given a float, prints it nicely. Localized floats must not be used in calculations!
* Based on Moodle's format_float.
*
* @param {any} float The float to print.
* @return {string} Locale float.
*/
formatFloat(float: any) : string {
if (typeof float == 'undefined') {
return '';
}
let localeSeparator = this.translate.instant('mm.core.decsep');
// Convert float to string.
float += '';
return float.replace('.', localeSeparator);
}
/**
* Returns a tree formatted from a plain list.
* List has to be sorted by depth to allow this function to work correctly. Errors can be thrown if a child node is
* processed before a parent node.
*
* @param {any[]} list List to format.
* @param {string} [parentFieldName=parent] Name of the parent field to match with children.
* @param {string} [idFieldName=id] Name of the children field to match with parent.
* @param {number} [rootParentId=0] The id of the root.
* @param {number} [maxDepth=5] Max Depth to convert to tree. Children found will be in the last level of depth.
* @return {any[]} Array with the formatted tree, children will be on each node under children field.
*/
formatTree(list: any[], parentFieldName = 'parent', idFieldName = 'id', rootParentId = 0, maxDepth = 5) : any[] {
let map = {},
mapDepth = {},
parent,
id,
tree = [];
list.forEach((node, index) => {
id = node[idFieldName];
parent = node[parentFieldName];
node.children = [];
// Use map to look-up the parents.
map[id] = index;
if (parent != rootParentId) {
let parentNode = list[map[parent]];
if (parentNode) {
if (mapDepth[parent] == maxDepth) {
// Reached max level of depth. Proceed with flat order. Find parent object of the current node.
let parentOfParent = parentNode[parentFieldName];
if (parentOfParent) {
// This element will be the child of the node that is two levels up the hierarchy
// (i.e. the child of node.parent.parent).
list[map[parentOfParent]].children.push(node);
// Assign depth level to the same depth as the parent (i.e. max depth level).
mapDepth[id] = mapDepth[parent];
// Change the parent to be the one that is two levels up the hierarchy.
node.parent = parentOfParent;
}
} else {
parentNode.children.push(node);
// Increase the depth level.
mapDepth[id] = mapDepth[parent] + 1;
}
}
} else {
tree.push(node);
// Root elements are the first elements in the tree structure, therefore have the depth level 1.
mapDepth[id] = 1;
}
});
return tree;
}
/**
* Get country name based on country code.
*
* @param {string} code Country code (AF, ES, US, ...).
* @return {string} Country name. If the country is not found, return the country code.
*/
getCountryName(code: string) : string {
let countryKey = 'mm.core.country-' + code,
countryName = this.translate.instant(countryKey);
return countryName !== countryKey ? countryName : code;
}
/**
* Get list of countries with their code and translated name.
*
* @return {Promise<any>} Promise resolved with the list of countries.
*/
getCountryList() : Promise<any> {
// Get the current language.
return this.langProvider.getCurrentLanguage().then((lang) => {
// Get the full list of translations. Create a promise to convert the observable into a promise.
return new Promise((resolve, reject) => {
let observer = this.translate.getTranslation(lang).subscribe((table) => {
resolve(table);
observer.unsubscribe();
}, (err) => {
reject(err);
observer.unsubscribe();
});
});
}).then((table) => {
let countries = {};
for (let name in table) {
if (name.indexOf('mm.core.country-') === 0) {
name = name.replace('mm.core.country-', '');
countries[name] = table[name];
}
}
return countries;
});
}
/**
* Given a list of files, check if there are repeated names.
*
* @param {any[]} files List of files.
* @return {string|boolean} String with error message if repeated, false if no repeated.
*/
hasRepeatedFilenames(files: any[]) : string|boolean {
if (!files || !files.length) {
return false;
}
let names = [];
// Check if there are 2 files with the same name.
for (let i = 0; i < files.length; i++) {
let name = files[i].filename || files[i].name;
if (names.indexOf(name) > -1) {
return this.translate.instant('mm.core.filenameexist', {$a: name});
} else {
names.push(name);
}
}
return false;
}
/**
* Gets the index of the first string that matches a regular expression.
*
* @param {string[]} array Array to search.
* @param {RegExp} regex RegExp to apply to each string.
* @return {number} Index of the first string that matches the RegExp. -1 if not found.
*/
indexOfRegexp(array: string[], regex: RegExp) : number {
if (!array || !array.length) {
return -1;
}
for (let i = 0; i < array.length; i++) {
let entry = array[i],
matches = entry.match(regex);
if (matches && matches.length) {
return i;
}
}
return -1;
}
/**
* Return true if the param is false (bool), 0 (number) or "0" (string).
*
* @param {any} value Value to check.
* @return {boolean} Whether the value is false, 0 or "0".
*/
isFalseOrZero(value: any) : boolean {
return typeof value != 'undefined' && (value === false || value === "false" || parseInt(value, 10) === 0);
}
/**
* Return true if the param is true (bool), 1 (number) or "1" (string).
*
* @param {any} value Value to check.
* @return {boolean} Whether the value is true, 1 or "1".
*/
isTrueOrOne(value: any) : boolean {
return typeof value != 'undefined' && (value === true || value === "true" || parseInt(value, 10) === 1);
}
/**
* Given an error returned by a WS call, check if the error is generated by the app or it has been returned by the WebSwervice.
*
* @param {string} error Error to check.
* @return {boolean} Whether the error was returned by the WebService.
*/
isWebServiceError(error: string) : boolean {
let localErrors = [
this.translate.instant('mm.core.wsfunctionnotavailable'),
this.translate.instant('mm.core.lostconnection'),
this.translate.instant('mm.core.userdeleted'),
this.translate.instant('mm.core.unexpectederror'),
this.translate.instant('mm.core.networkerrormsg'),
this.translate.instant('mm.core.serverconnection'),
this.translate.instant('mm.core.errorinvalidresponse'),
this.translate.instant('mm.core.sitemaintenance'),
this.translate.instant('mm.core.upgraderunning'),
this.translate.instant('mm.core.nopasswordchangeforced'),
this.translate.instant('mm.core.unicodenotsupported')
];
return error && localErrors.indexOf(error) == -1;
}
/**
* Merge two arrays, removing duplicate values.
*
* @param {any[]} array1 The first array.
* @param {any[]} array2 The second array.
* @param [key] Key of the property that must be unique. If not specified, the whole entry.
* @return {any[]} Merged array.
*/
mergeArraysWithoutDuplicates(array1: any[], array2: any[], key?: string) : any[] {
return this.uniqueArray(array1.concat(array2), key);
}
/**
* Open a file using platform specific method.
*
* node-webkit: Using the default application configured.
* Android: Using the WebIntent plugin.
* iOs: Using handleDocumentWithURL.
*
* @param {string} path The local path of the file to be open.
* @return {Promise<any>} Promise resolved when done.
*/
openFile(path: string) : Promise<any> {
return new Promise((resolve, reject) => {
if (this.appProvider.isDesktop()) {
// It's a desktop app, send an event so the file is opened. It has to be done with an event
// because opening the file from here (renderer process) doesn't focus the opened app.
// Use sendSync so we can receive the result.
if (require('electron').ipcRenderer.sendSync('openItem', path)) {
resolve();
} else {
reject(this.translate.instant('mm.core.erroropenfilenoapp'));
}
} else if ((<any>window).plugins) {
// @todo
reject('TODO');
// var extension = $mmFS.getFileExtension(path),
// mimetype = $mmFS.getMimeType(extension);
// if (ionic.Platform.isAndroid() && window.plugins.webintent) {
// var iParams = {
// action: "android.intent.action.VIEW",
// url: path,
// type: mimetype
// };
// window.plugins.webintent.startActivity(
// iParams,
// function() {
// $log.debug('Intent launched');
// deferred.resolve();
// },
// function() {
// $log.debug('Intent launching failed.');
// $log.debug('action: ' + iParams.action);
// $log.debug('url: ' + iParams.url);
// $log.debug('type: ' + iParams.type);
// if (!extension || extension.indexOf('/') > -1 || extension.indexOf('\\') > -1) {
// // Extension not found.
// $mmLang.translateAndRejectDeferred(deferred, 'mm.core.erroropenfilenoextension');
// } else {
// $mmLang.translateAndRejectDeferred(deferred, 'mm.core.erroropenfilenoapp');
// }
// }
// );
// } else if (ionic.Platform.isIOS() && typeof handleDocumentWithURL == 'function') {
// $mmFS.getBasePath().then(function(fsRoot) {
// // Encode/decode the specific file path, note that a path may contain directories
// // with white spaces, special characters...
// if (path.indexOf(fsRoot > -1)) {
// path = path.replace(fsRoot, "");
// path = encodeURIComponent($mmText.decodeURIComponent(path));
// path = fsRoot + path;
// }
// handleDocumentWithURL(
// function() {
// $log.debug('File opened with handleDocumentWithURL' + path);
// deferred.resolve();
// },
// function(error) {
// $log.debug('Error opening with handleDocumentWithURL' + path);
// if(error == 53) {
// $log.error('No app that handles this file type.');
// }
// self.openInBrowser(path);
// deferred.resolve();
// },
// path
// );
// }, deferred.reject);
// } else {
// // Last try, launch the file with the browser.
// this.openInBrowser(path);
// resolve();
// }
} else {
// Changing _blank for _system may work in cordova 2.4 and onwards.
this.logger.log('Opening external file using window.open()');
window.open(path, '_blank');
resolve();
}
});
}
/**
* Open a URL using InAppBrowser.
* Do not use for files, refer to {@link openFile}.
*
* @param {string} url The URL to open.
* @param {any} [options] Override default options passed to InAppBrowser.
* @return {InAppBrowserObject} The opened window.
*/
openInApp(url: string, options?: any) : InAppBrowserObject {
if (!url) {
return;
}
options = options || {};
if (!options.enableViewPortScale) {
options.enableViewPortScale = 'yes'; // Enable zoom on iOS.
}
if (!options.location && this.platform.is('ios') && url.indexOf('file://') === 0) {
// The URL uses file protocol, don't show it on iOS.
// In Android we keep it because otherwise we lose the whole toolbar.
options.location = 'no';
}
this.iabInstance = this.iab.create(url, '_blank', options);
return this.iabInstance;
}
/**
* Open a URL using a browser.
* Do not use for files, refer to {@link openFile}.
*
* @param {string} url The URL to open.
*/
openInBrowser(url: string) : void {
if (this.appProvider.isDesktop()) {
// It's a desktop app, use Electron shell library to open the browser.
let shell = require('electron').shell;
if (!shell.openExternal(url)) {
// Open browser failed, open a new window in the app.
window.open(url, '_system');
}
} else {
window.open(url, '_system');
}
}
/**
* Open an online file using platform specific method.
* Specially useful for audio and video since they can be streamed.
*
* node-webkit: Using the default application configured.
* Android: Using the WebIntent plugin.
* iOS: Using the window.open method (InAppBrowser)
* We don't use iOS quickview framework because it doesn't support streaming.
*
* @param {string} url The URL of the file.
* @return {Promise<void>} Promise resolved when opened.
*/
openOnlineFile(url: string) : Promise<void> {
return new Promise<void>((resolve, reject) => {
// @todo
reject('TODO');
// if (ionic.Platform.isAndroid() && window.plugins && window.plugins.webintent) {
// // In Android we need the mimetype to open it.
// var iParams;
// self.getMimeTypeFromUrl(url).catch(function() {
// // Error getting mimetype, return undefined.
// }).then(function(mimetype) {
// if (!mimetype) {
// // Couldn't retrieve mimetype. Return error.
// $mmLang.translateAndRejectDeferred(deferred, 'mm.core.erroropenfilenoextension');
// return;
// }
// iParams = {
// action: "android.intent.action.VIEW",
// url: url,
// type: mimetype
// };
// window.plugins.webintent.startActivity(
// iParams,
// function() {
// $log.debug('Intent launched');
// deferred.resolve();
// },
// function() {
// $log.debug('Intent launching failed.');
// $log.debug('action: ' + iParams.action);
// $log.debug('url: ' + iParams.url);
// $log.debug('type: ' + iParams.type);
// $mmLang.translateAndRejectDeferred(deferred, 'mm.core.erroropenfilenoapp');
// }
// );
// });
// } else {
// this.logger.log('Opening remote file using window.open()');
// window.open(url, '_blank');
// resolve();
// }
});
}
/**
* Converts an object into an array, losing the keys.
*
* @param {object} obj Object to convert.
* @return {any[]} Array with the values of the object but losing the keys.
*/
objectToArray(obj: object) : any[] {
return Object.keys(obj).map((key) => {
return obj[key];
});
}
/**
* Converts an object into an array of objects, where each entry is an object containing
* the key and value of the original object.
* For example, it can convert {size: 2} into [{name: 'size', value: 2}].
*
* @param {object} obj Object to convert.
* @param {string} keyName Name of the properties where to store the keys.
* @param {string} valueName Name of the properties where to store the values.
* @param {boolean} [sort] True to sort keys alphabetically, false otherwise.
* @return {object[]} Array of objects with the name & value of each property.
*/
objectToArrayOfObjects(obj: object, keyName: string, valueName: string, sort?: boolean) : object[] {
// Get the entries from an object or primitive value.
let getEntries = (elKey, value) => {
if (typeof value == 'object') {
// It's an object, return at least an entry for each property.
let keys = Object.keys(value),
entries = [];
keys.forEach((key) => {
let newElKey = elKey ? elKey + '[' + key + ']' : key;
entries = entries.concat(getEntries(newElKey, value[key]));
});
return entries;
} else {
// Not an object, return a single entry.
let entry = {};
entry[keyName] = elKey;
entry[valueName] = value;
return entry;
}
};
if (!obj) {
return [];
}
// "obj" will always be an object, so "entries" will always be an array.
let entries = <any[]> getEntries('', obj);
if (sort) {
return entries.sort((a, b) => {
return a.name >= b.name ? 1 : -1;
});
}
return entries;
}
/**
* Converts an array of objects into an object with key and value. The opposite of objectToArrayOfObjects.
* For example, it can convert [{name: 'size', value: 2}] into {size: 2}.
*
* @param {object[]} objects List of objects to convert.
* @param {string} keyName Name of the properties where the keys are stored.
* @param {string} valueName Name of the properties where the values are stored.
* @param [keyPrefix] Key prefix if neededs to delete it.
* @return {object} Object.
*/
objectToKeyValueMap(objects: object[], keyName: string, valueName: string, keyPrefix?: string) : object {
let prefixSubstr = keyPrefix ? keyPrefix.length : 0,
mapped = {};
objects.forEach((item) => {
let key = prefixSubstr > 0 ? item[keyName].substr(prefixSubstr) : item[keyName];
mapped[key] = item[valueName];
});
return mapped;
}
/**
* Given an observable, convert it to a Promise that will resolve with the first received value.
*
* @param {Observable<any>} obs The observable to convert.
* @return {Promise<any>} Promise.
*/
observableToPromise(obs: Observable<any>) : Promise<any> {
return new Promise((resolve, reject) => {
let subscription = obs.subscribe((data) => {
// Data received, unsubscribe.
subscription.unsubscribe();
resolve(data);
}, (error) => {
// Data received, unsubscribe.
subscription.unsubscribe();
reject(error);
});
});
}
/**
* Similar to AngularJS $q.defer().
*
* @return {PromiseDefer} The deferred promise.
*/
promiseDefer() : PromiseDefer {
let deferred: PromiseDefer = {};
deferred.promise = new Promise((resolve, reject) => {
deferred.resolve = resolve;
deferred.reject = reject;
});
return deferred;
}
/**
* Given a promise, returns true if it's rejected or false if it's resolved.
*
* @param {Promise<any>} promise Promise to check
* @return {Promise<boolean>} Promise resolved with boolean: true if the promise is rejected or false if it's resolved.
*/
promiseFails(promise: Promise<any>) : Promise<boolean> {
return promise.then(() => {
return false;
}).catch(() => {
return true;
});
}
/**
* Given a promise, returns true if it's resolved or false if it's rejected.
*
* @param {Promise<any>} promise Promise to check
* @return {Promise<boolean>} Promise resolved with boolean: true if the promise it's resolved or false if it's rejected.
*/
promiseWorks(promise: Promise<any>) : Promise<boolean> {
return promise.then(() => {
return true;
}).catch(() => {
return false;
});
}
/**
* Tests to see whether two arrays or objects have the same value at a particular key.
* Missing values are replaced by '', and the values are compared with ===.
* Booleans and numbers are cast to string before comparing.
*
* @param {any} obj1 The first object or array.
* @param {any} obj2 The second object or array.
* @param {string} key Key to check.
* @return {boolean} Whether the two objects/arrays have the same value (or lack of one) for a given key.
*/
sameAtKeyMissingIsBlank(obj1: any, obj2: any, key: string) : boolean {
let value1 = typeof obj1[key] != 'undefined' ? obj1[key] : '',
value2 = typeof obj2[key] != 'undefined' ? obj2[key] : '';
if (typeof value1 == 'number' || typeof value1 == 'boolean') {
value1 = '' + value1;
}
if (typeof value2 == 'number' || typeof value2 == 'boolean') {
value2 = '' + value2;
}
return value1 === value2;
}
/**
* Stringify an object, sorting the properties. It doesn't sort arrays, only object properties. E.g.:
* {b: 2, a: 1} -> '{"a":1,"b":2}'
*
* @param {object} obj Object to stringify.
* @return {string} Stringified object.
*/
sortAndStringify(obj: object) : string {
return JSON.stringify(obj, Object.keys(this.flattenObject(obj)).sort());
}
/**
* Sum the filesizes from a list of files checking if the size will be partial or totally calculated.
*
* @param {any[]} files List of files to sum its filesize.
* @return {object} Object with the file size and a boolean to indicate if it is the total size or only partial.
*/
sumFileSizes(files: any[]) : object {
let result = {
size: 0,
total: true
};
files.forEach((file) => {
if (typeof file.filesize == 'undefined') {
// We don't have the file size, cannot calculate its total size.
result.total = false;
} else {
result.size += file.filesize;
}
});
return result;
}
/**
* Converts locale specific floating point/comma number back to standard PHP float value.
* Do NOT try to do any math operations before this conversion on any user submitted floats!
* Based on Moodle's unformat_float function.
*
* @param {any} localeFloat Locale aware float representation.
* @return {any} False if bad format, empty string if empty value or the parsed float if not.
*/
unformatFloat(localeFloat: any) : any {
// Bad format on input type number.
if (typeof localeFloat == "undefined") {
return false;
}
// Empty (but not zero).
if (localeFloat == null) {
return '';
}
// Convert float to string.
localeFloat += '';
localeFloat = localeFloat.trim();
if (localeFloat == '') {
return '';
}
let localeSeparator = this.translate.instant('mm.core.decsep');
localeFloat = localeFloat.replace(' ', ''); // No spaces - those might be used as thousand separators.
localeFloat = localeFloat.replace(localeSeparator, '.');
localeFloat = parseFloat(localeFloat);
// Bad format.
if (isNaN(localeFloat)) {
return false;
}
return localeFloat;
}
/**
* Return an array without duplicate values.
*
* @param {any[]} array The array to treat.
* @param [key] Key of the property that must be unique. If not specified, the whole entry.
* @return {any[]} Array without duplicate values.
*/
uniqueArray(array: any[], key?: string) : any[] {
let filtered = [],
unique = [],
len = array.length;
for (let i = 0; i < len; i++) {
let entry = array[i],
value = key ? entry[key] : entry;
if (unique.indexOf(value) == -1) {
unique.push(value);
filtered.push(entry);
}
}
return filtered;
}
}