UNPKG

@plone/volto

Version:
378 lines (347 loc) 10.6 kB
import cloneDeepWith from 'lodash/cloneDeepWith'; import flatten from 'lodash/flatten'; import isEqual from 'lodash/isEqual'; import isObject from 'lodash/isObject'; import transform from 'lodash/transform'; import React from 'react'; import { matchPath } from 'react-router'; import config from '@plone/volto/registry'; /** * Deep diff between two object, using lodash * @param {Object} object Object compared * @param {Object} base Object to compare with * @return {Object} Return a new object who represent the diff */ export function difference(object, base) { return transform(object, (result, value, key) => { if (!isEqual(value, base[key])) { result[key] = isObject(value) && isObject(base[key]) ? difference(value, base[key]) : value; } }); } /** * Throw an error if the wrapped function returns undefined * * @param {Function} func */ export const safeWrapper = (func) => (config) => { const res = func(config); if (typeof res === 'undefined') { throw new Error(`Configuration function doesn't return config, ${func}`); } return res; }; /** * A helper to pipe a configuration object through configuration loaders * * @param {Array} configMethods A list of configuration methods * @param {Object} config The Volto singleton config object */ export function applyConfig(configMethods, config) { return configMethods.reduce((acc, apply) => safeWrapper(apply)(acc), config); } /** * A HOC factory that propagates the status of asyncConnected requests back to * the main server process, to allow properly expressing an error status as * HTTP status code * * @param {} code HTTP return code */ export function withServerErrorCode(code) { return (WrappedComponent) => (props) => { if (props.staticContext && Object.keys(props.staticContext).length === 0) { const { staticContext } = props; staticContext.error_code = code; staticContext.error = props.error; } return <WrappedComponent {...props} />; }; } // See https://en.wikipedia.org/wiki/Web_colors#Extended_colors const safeColors = [ 'Black', 'Blue', 'BlueViolet', 'Brown', 'Crimson', 'DarkBlue', 'DarkCyan', 'DarkGreen', 'DarkMagenta', 'DarkOliveGreen', 'DarkOrchid', 'DarkRed', 'DarkSlateBlue', 'DarkSlateGray', 'DarkViolet', 'DeepPink', 'DimGray', 'DodgerBlue', 'Firebrick', 'ForestGreen', 'Fuchsia', 'Green', 'IndianRed', 'Indigo', 'Magenta', 'Maroon', 'MediumBlue', 'MediumSlateBlue', 'MediumVioletRed', 'MidnightBlue', 'Navy', 'Olive', 'OliveDrab', 'OrangeRed', 'Purple', 'Red', 'RoyalBlue', 'SaddleBrown', 'SeaGreen', 'Sienna', 'SlateBlue', 'SlateGray', 'SteelBlue', 'Teal', ]; const namedColors = {}; /** * Will generate initials from string * @param {string} name * @param {integer} count * @returns {string} only one letter if received only one name */ export const getInitials = (title, limit) => { const text = title .split(' ') .map((n) => (n[0] ? n[0].toUpperCase() : '')) .join(''); if (limit) { return text.substring(0, limit); } return text; }; /** * Will generate a random color hex * Will also remmember the color for each userId * @param {string} userId */ export const getColor = (name) => { const namedColor = namedColors[name] ? namedColors[name] : safeColors.length > 0 ? safeColors.pop() : `#${Math.floor(Math.random() * 16777215).toString(16)}`; if (!namedColors[name]) { namedColors[name] = namedColor; } return namedColor; }; /** * Fixes TimeZones issues on moment date objects * Parses a DateTime and sets correct moment locale * @param {string} locale Current locale * @param {string} value DateTime string * @param {string} format Date format of choice * @returns {Object|string} Moment object or string if format is set */ export const parseDateTime = (locale, value, format, moment) => { // Used to set a server timezone or UTC as default moment.updateLocale(locale, moment.localeData(locale)._config); // copy locale to moment-timezone let datetime = null; if (value) { // check if datetime has timezone, otherwise assumes it's UTC datetime = !value.match(/T/) || value.match(/T(.)*(-|\+|Z)/g) ? // Since we assume UTC everywhere, then transform to local (momentjs default) moment(value) : // This might happen in old Plone versions dates moment(`${value}Z`); } if (format && datetime) { return datetime.format(format); } return datetime; }; /** * Converts a language code like pt-br to the format `pt_BR` (`lang_region`) * Useful for passing from Plone's i18n lang names to Xnix locale names * eg. LC_MESSAGES/lang_region.po filenames. Also used in the I18N_LANGUAGE cookie. * @param {string} language Language to be converted * @returns {string} Language converted */ export const toGettextLang = (language) => { if (language.includes('-')) { let normalizedLang = language.split('-'); normalizedLang = `${normalizedLang[0]}_${normalizedLang[1].toUpperCase()}`; return normalizedLang; } return language; }; export const normalizeLanguageName = toGettextLang; /** * Converts a language code like pt-br or pt_BR to the format `pt-BR`. * `react-intl` only supports this syntax. We also use it for the locales * in the volto Redux store. * @param {string} language Language to be converted * @returns {string} Language converted */ export const toReactIntlLang = (language) => { if (language.includes('_') || language.includes('-')) { let langCode = language.split(/[-_]/); langCode = `${langCode[0]}-${langCode[1].toUpperCase()}`; return langCode; } return language; }; export const toLangUnderscoreRegion = toReactIntlLang; // old name for backwards-compat /** * Converts a language code like pt_BR or pt-BR to the format `pt-br`. * This format is used on the backend and in volto config settings. * @param {string} language Language to be converted * @returns {string} Language converted */ export const toBackendLang = (language) => { return toReactIntlLang(language).toLowerCase(); }; /** * Lookup if a given expander is set in apiExpanders for the given path and action type * @param {string} expander The id literal of the expander eg. `navigation` * @param {string} path The path (no URL) to check if the expander has effect * @param {string} type The Redux action type * @returns {boolean} Return if the expander is present for the path and the type given */ export const hasApiExpander = (expander, path = '', type = 'GET_CONTENT') => { return flatten( config.settings.apiExpanders .filter((expand) => matchPath(path, expand.match) && expand[type]) .map((expand) => expand[type]), ).includes(expander); }; /** * Insert element into array at a give index * @param {Array} array Array with data * @param {*} element Element to be inserted * @param {number} index Index of item to be inserted at * @returns {Array} Array with inserted element */ export const insertInArray = (array, element, index) => [ ...array.slice(0, index), element, ...array.slice(index), ]; /** * Replace element in array at a give index * @param {Array} array Array with data * @param {*} element Element to be replaced * @param {number} index Index of item to be replaced at * @returns {Array} Array with replaced element */ export const replaceItemOfArray = (array, index, value) => Object.assign([...array], { [index]: value }); /** * Remove item from array at given index * @param {Array} array Array with data * @param {number} index Index of item to be removed * @returns {Array} Array without deleted element */ export const removeFromArray = (array, index) => { let newArray = array.slice(); newArray.splice(index, 1); return newArray; }; /** * Moves an item from origin to target inside an array in an immutable way * @param {Array} array Array with data * @param {number} origin Index of item to be moved from * @param {number} target Index of item to be moved to * @returns {Array} Resultant array */ export const reorderArray = (array, origin, target) => { const result = Array.from(array); const [removed] = result.splice(origin, 1); result.splice(target, 0, removed); return result; }; /** * Normalize (unicode) string to a normalized plain ascii string * @method normalizeString * @param {string} str The string to be normalized * @returns {string} Normalized plain ascii string */ export function normalizeString(str) { return str.normalize('NFD').replace(/\p{Diacritic}/gu, ''); } /** * Slugify a string: remove whitespaces, special chars and replace with _ * @param {string} string String to be slugified * @param {Array} slugs Array with slugs already taken * @returns {string} Slugified string */ export const slugify = (string, slugs = []) => { let slug = string .toLowerCase() .replace(/[\s-]+/g, '_') .replace(/[^\w]+/g, ''); let i = 1; if (slugs.includes(slug)) { while (slugs.includes(`${slug}_${i}`)) { i++; } slug = `${slug}_${i}`; } return slug; }; /** * cloneDeep an object with support for JSX nodes on it * Somehow, in a browser it fails with a "Illegal invocation" error * but in node (Jest test) it doesn't. This does the trick. * @param {object} object object to be cloned * @returns {object} deep cloned object */ export const cloneDeepSchema = (object) => { return cloneDeepWith(object, (value) => { if (React.isValidElement(value)) { // If a JSX valid element, just return it, do not try to deep clone it return value; } }); }; /** * Creates an array given a range of numbers * @param {number} start start number from * @param {number} stop stop number at * @param {number} step step every each number in the sequence * @returns {array} The result, eg. [0, 1, 2, 3, 4] */ export const arrayRange = (start, stop, step) => Array.from( { length: (stop - start) / step + 1 }, (value, index) => start + index * step, ); /** * Given an event target element returns if it's an interactive element * of the one in the list. * @param {node} element event.target element type * @returns {boolean} If it's an interactive element of the list */ export function isInteractiveElement( element, interactiveElements = [ 'button', 'input', 'textarea', 'select', 'option', 'svg', 'path', ], ) { if (interactiveElements.includes(element.tagName.toLowerCase())) { return true; } return false; }