UNPKG

comindware.ui

Version:

Comindware Core UI provides the basic components like editors, lists, dropdowns, popups that we so desperately need while creating Marionette-based single-page applications.

365 lines (340 loc) 16.2 kB
/** * Developer: Stepan Burguchev * Date: 8/21/2014 * Copyright: 2009-2016 Comindware® * All Rights Reserved * Published under the MIT license */ 'use strict'; import 'lib'; import LocalizationService from '../services/LocalizationService'; const timeoutCache = {}; const queueCache = {}; let getPluralFormIndex = null; export default /** @lends module:core.utils.helpers */ { /** * Deprecated. Use <code>_.debounce()</code> instead. Defers invoking the function until after `delay` milliseconds * have elapsed since the last time it was invoked. * @param {String} someUniqueId Function identifier. * @param {Function} callback The function tobe called after delay. * @param {Number} delay Callback delay in milliseconds. * @deprecated * */ setUniqueTimeout(someUniqueId, callback, delay) { const timeoutId = timeoutCache[someUniqueId]; if (timeoutId) { clearTimeout(timeoutId); } const handler = setTimeout(() => { callback(); delete timeoutCache[someUniqueId]; }, delay); timeoutCache[someUniqueId] = handler; return handler; }, /** * Deprecated. Use <code>_.defer()</code> instead. Defers invoking the function until the current call stack has cleared. * @param {Function} callback Callback to be called when the current call stack has cleared. * @deprecated * */ nextTick(callback) { return setTimeout(callback, 10); }, /** * Creates and returns a new function that maps the passed comparator onto the specified attribute of Backbone.Model. * Look at the example for details. * @example * var referenceComparator = core.utils.helpers.comparatorFor(core.utils.comparators.stringComparator2Asc, 'text'); * var a = new Backbone.Model({ id: 2, text: '1' }); * var b = new Backbone.Model({ id: 1, text: '2' }); * // returns -1 * var result = referenceComparator(a, b); * @param {Function} comparatorFn Wrapped comparator function. 1 or 2 arguments. * @param {String} propertyName Attribute of a Backbone.Model to which the function is mapped. * @return {Function} Result function. * */ comparatorFor(comparatorFn, propertyName) { if (comparatorFn.length === 1) { return function(a) { return comparatorFn(a.get(propertyName)); }; } else if (comparatorFn.length === 2) { return function(a, b) { return comparatorFn(a.get(propertyName), b.get(propertyName)); }; } throw new Error('Invalid arguments count in comparator function.'); }, /** * Accepts string and duplicates it into every field of LocalizedText object. * The LocalizedText looks like this: <code>{ en: 'foo', de: 'foo', ru: 'foo' }</code>. * @param {String} defaultText A text that is set into each field of the resulting LocalizedText object. * @return {Object} LocalizedText object like <code>{ en, de, ru }</code>. * */ createLocalizedText(defaultText) { return { en: defaultText, de: defaultText, ru: defaultText }; }, /** * Javascript version of the Microsoft .NET framework method <code>string.Format</code>. * @example * // returns 'Hello, Javascript!' * core.utils.helpers.format('Hello, {0}!', 'Javascript'); * @param {String} text Formatted text that contains placeholders like <code>{i}</code>. * Where <code>i</code> - index of the inserted argument (starts from zero). * @param {...*} arguments Arguments that will replace the placeholders in text. * @return {String} Resulting string. * */ format(text) { if (!_.isString(text)) { return ''; } for (let i = 1; i < arguments.length; i++) { const regexp = new RegExp(`\\{${i - 1}\\}`, 'gi'); text = text.replace(regexp, arguments[i]); } return text; }, /** * Takes a number and array of strings and then returns a valid plural form. * Works with complex cases and valid for all supported languages (by default for en, de and ru). * The core algorithm is located in localization text `CORE.SERVICES.LOCALIZATION.PLURALFORM`. * @function * @example * // returns 'car' * core.utils.helpers.getPluralForm(1, 'car,cars'); * // returns 'cars' * core.utils.helpers.getPluralForm(10, 'car,cars'); * @param {Number} n A number which requires a correct work form. * @param {String} texts Comma separated string of word forms. * (2 word forms for en and de, 3 word forms for ru). * @return {String} Resulting string. * */ getPluralForm(n, texts) { if (!getPluralFormIndex) { const formula = LocalizationService.get('CORE.SERVICES.LOCALIZATION.PLURALFORM'); getPluralFormIndex = new Function('n', `var r = ${formula};return typeof r !== 'boolean' ? r : r === true ? 1 : 0;`); // jshint ignore:line } return texts.split(',')[getPluralFormIndex(n)]; }, /** * Creates a queue of asynchronous operations. * New operation function is executed only after the previous function with the same id has executed. * @example * var save = form.save.bind(form); * // Three sequential calls * var promise1 = core.utils.helpers.enqueueOperation(save, 42); * var promise2 = core.utils.helpers.enqueueOperation(save, 42); * var promise3 = core.utils.helpers.enqueueOperation(save, 42); * promise3.then(function () { * // Will be called only when all the 'save' operations has been fired and returned success. * }); * @param {Function} operation A function that triggers asynchronous operation and returns a Promise object. * @param {String} queueId String identifier of operations queue. * */ enqueueOperation(operation, queueId) { if (queueCache[queueId] && queueCache[queueId].isPending()) { queueCache[queueId] = queueCache[queueId].then(() => _.isFunction(operation) ? operation() : operation); } else { queueCache[queueId] = Promise.resolve(_.isFunction(operation) ? operation() : operation); } return queueCache[queueId]; }, /** * Sequentially applies passed Behavior objects on to the given instance. * The method has nothing to do with Marionette.Behavior. * @example * core.utils.helpers.applyBehavior(this, core.models.behaviors.SelectableBehavior); * @param {Object} target Target instance that is getting behaviors applied. * @param {...Function} arguments 1 or more Behavior objects (constructor functions). * */ applyBehavior(target) { const behaviors = _.rest(arguments, 1); _.each(behaviors, Behavior => { _.extend(target, new Behavior(target)); }); }, /** * Allows to perform validation of input options. The method is usually used in constructor or initializer methods. * Allows to check both direct and nested properties of the options object. * Throws <code>MissingOptionError</code> if the attribute is undefined. * @example * // Checks that property options.model exists. * core.utils.helpers.ensureOption(options, 'model'); * // Checks that property options.property1.subProperty exists. * core.utils.helpers.ensureOption(options, 'property1.subProperty'); * @param {Object} options Options object to check. * @param {String} optionName Property name or dot-separated property path. * */ ensureOption(options, optionName) { if (!options) { this.throwError('The options object is required.', 'MissingOptionError'); } if (optionName.indexOf('.') !== -1) { const selector = optionName.split('.'); for (let i = 0, len = selector.length; i < len; i++) { optionName = selector[i]; if (options[optionName] === undefined) { optionName = _.take(selector, i + 1).join('.'); this.throwError(`The option \`${optionName}\` is required.`, 'MissingOptionError'); } options = options[optionName]; } } else if (options[optionName] === undefined) { this.throwError(`The option \`${optionName}\` is required.`, 'MissingOptionError'); } }, /** * Allows to perform validation of property in an object. Allows to check both direct and nested properties of the object. * Throws <code>MissingOptionError</code> if the attribute is undefined. * @example * // Checks that property this.view.moduleRegion exists. * core.utils.helpers.ensureOption(this.view, 'moduleRegion'); * @param {Object} object An object to check. * @param {String} propertyName Property name or dot-separated property path. * */ ensureProperty(object, propertyName) { if (!object) { this.throwError('The object is null.', 'NullObjectError'); } if (propertyName.indexOf('.') !== -1) { const selector = propertyName.split('.'); for (let i = 0, len = selector.length; i < len; i++) { propertyName = selector[i]; if (object[propertyName] === undefined) { propertyName = _.take(selector, i + 1).join('.'); this.throwError(`The property \`${propertyName}\` is required.`, 'MissingPropertyError'); } object = object[propertyName]; } } else if (object[propertyName] === undefined) { this.throwError(`The property \`${propertyName}\` is required.`, 'MissingPropertyError'); } }, /** * Allows to retrieve a property (or subproperty) of an object. Does not throw any error if one of the properties along the way are missing. * Doesn't throw if the object itself is undefined. * @example * var foo = { a: {} }; * // returns undefined (doesn't throw an error) * core.utils.helpers.getPropertyOrDefault(foo, 'a.b.c.d'); * @param {String} propertyPath propertyName Property name or dot-separated property path. * @param {Object} obj An object to get the property from. * */ getPropertyOrDefault(propertyPath, obj) { return [obj].concat(propertyPath.split('.')).reduce((prev, curr) => prev === undefined ? undefined : prev[curr]); }, /** * Pre-validation helper that allows to check that function argument is not falsy. * Falsy value means that the value is one of the following: <code>undefined, null, 0, '', false</code>. * Throws <code>ArgumentFalsyError</code> if validation is failed. * @example * core.utils.helpers.assertArgumentNotFalsy(argument1, 'argument1'); * @param {*} argumentValue Value to check. * @param {String} argumentName Name of the checked argument. Needs to specify in the exception text. * */ assertArgumentNotFalsy(argumentValue, argumentName) { if (!argumentValue) { this.throwError(`Argument \`${argumentName}\` is falsy.`, 'ArgumentFalsyError'); } }, /** * Simplified way to throw an error. Throws an Error object with the specified name and message. * @example * core.utils.helpers.throwError('Request is invalid.'); * @param {String} message Error message. * @param {String} [name='Error'] Error name (`name` attribute of Error object). * */ throwError(message, name) { const error = new Error(message); error.name = name || 'Error'; throw error; }, /** * Throws InvalidOperationError. The exception should be thrown when a class is in invalid state to call the checked method. * @example * // Inside of implementation of some Marionette.View. * addKeyboardListener: function (key, callback) { * if (!this.keyListener) { * utils.helpers.throwInvalidOperationError('You must apply keyboard listener after \'render\' event has happened.'); * } * var keys = key.split(','); * _.each(keys, function (k) { * this.keyListener.simple_combo(k, callback); * }, this); * }, * // ... * @param {String} [message='Invalid operation'] Error message. * */ throwInvalidOperationError(message) { this.throwError(message || 'Invalid operation', 'InvalidOperationError'); }, /** * Throws FormatError. The exception should be thrown when the format of an argument is invalid, or when a is not well formed. * @example * function (url, parameterNames, parameters, callback) { * // Some code here ... * if (parameters.Length !== parameterNames.length) { * utils.helpers.throwFormatError('The arrays `parameters` and `parameterNames` should have identical length.'); * } * // Some code here ... * @param {String} [message='Invalid format'] Error message. * */ throwFormatError(message) { this.throwError(message || 'Invalid format', 'FormatError'); }, /** * Throws ArgumentError. The exception should be thrown when one of the arguments provided to a method is not valid. * Should be thrown only when one particular argument is invalid. If a combination of arguments is invalid use <code>FormatError</code>. * @example * function (url, parameterNames, parameters, callback) { * // Some code here ... * if (parameterNames.Length !== 2) { * utils.helpers.throwArgumentError('The array `parameterNames` should contain exactly 2 elements.'); * } * // Some code here ... * @param {String} [message='Invalid argument'] Error message. * */ throwArgumentError(message) { this.throwError(message || 'Invalid argument', 'ArgumentError'); }, /** * Throws NotSupportedError. The exception should be thrown when an invoked method is not supported. * For example: some class doesn't support all the methods of the interface it implements. * @example * // Inside of implementation of some Stream class * seek() { * // Some code here ... * utils.helpers.throwNotSupportedError('The network stream doesn't support `seek`.'); * // Some code here ... * } * @param {String} [message='The operation is not supported'] Error message. * */ throwNotSupportedError(message) { this.throwError(message || 'The operation is not supported', 'NotSupportedError'); }, /** * Throws NotImplementedError. The exception should be thrown when a requested method or operation is not implemented. * For example: a base class could have abstract methods that throws such error. * @example * // Inside of implementation of some base controller class. * navigate() { * utils.this.throwNotImplementedError(); * } * @param {String} [message='The operation is not implemented'] Error message. * */ throwNotImplementedError(message) { this.throwError(message || 'The operation is not implemented', 'NotImplementedError'); }, /** * Throws NotFoundError. The exception should be thrown when a requested object could not be found. * For example: we looked up in the database and could find a person with requested id. * @param {String} [message='Object not found'] Error message. * */ throwNotFoundError(message) { this.throwError(message || 'Object not found', 'NotFoundError'); } };