2017-11-06 17:17:11 +01:00
|
|
|
|
// (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 { 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';
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* "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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-07 13:01:32 +01:00
|
|
|
|
/**
|
|
|
|
|
* Similar to AngularJS $q.defer(). It will return an object containing the promise, and the resolve and reject functions.
|
|
|
|
|
*
|
|
|
|
|
* @return {any} Object containing the promise, and the resolve and reject functions.
|
|
|
|
|
*/
|
|
|
|
|
promiseDefer() : any {
|
|
|
|
|
let deferred: any = {};
|
|
|
|
|
deferred.promise = new Promise((resolve, reject) => {
|
|
|
|
|
deferred.resolve = resolve;
|
|
|
|
|
deferred.reject = reject;
|
|
|
|
|
});
|
|
|
|
|
return deferred;
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-06 17:17:11 +01:00
|
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Serialize an object to be used in a request.
|
|
|
|
|
*
|
|
|
|
|
* @param {any} obj Object to serialize.
|
|
|
|
|
* @param {boolean} [addNull] Add null values to the serialized as empty parameters.
|
|
|
|
|
* @return {string} Serialization of the object.
|
|
|
|
|
*/
|
|
|
|
|
serialize(obj: any, addNull?: boolean) : string {
|
|
|
|
|
let query = '',
|
|
|
|
|
fullSubName,
|
|
|
|
|
subValue,
|
|
|
|
|
innerObj;
|
|
|
|
|
|
|
|
|
|
for (let name in obj) {
|
|
|
|
|
let value = obj[name];
|
|
|
|
|
|
|
|
|
|
if (value instanceof Array) {
|
|
|
|
|
for (let i = 0; i < value.length; ++i) {
|
|
|
|
|
subValue = value[i];
|
|
|
|
|
fullSubName = name + '[' + i + ']';
|
|
|
|
|
innerObj = {};
|
|
|
|
|
innerObj[fullSubName] = subValue;
|
|
|
|
|
query += this.serialize(innerObj) + '&';
|
|
|
|
|
}
|
|
|
|
|
} else if (value instanceof Object) {
|
|
|
|
|
for (let subName in value) {
|
|
|
|
|
subValue = value[subName];
|
|
|
|
|
fullSubName = name + '[' + subName + ']';
|
|
|
|
|
innerObj = {};
|
|
|
|
|
innerObj[fullSubName] = subValue;
|
|
|
|
|
query += this.serialize(innerObj) + '&';
|
|
|
|
|
}
|
|
|
|
|
} else if (addNull || (typeof value != 'undefined' && value !== null)) {
|
|
|
|
|
query += encodeURIComponent(name) + '=' + encodeURIComponent(value) + '&';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return query.length ? query.substr(0, query.length - 1) : query;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
}
|
|
|
|
|
}
|