UNPKG

@eightshift/frontend-libs

Version:

A collection of useful frontend utility modules. powered by Eightshift

428 lines (383 loc) 11.8 kB
import { camelCase, has, isEmpty, lowerFirst, upperFirst } from '@eightshift/ui-components/utilities'; /** * Sets attributes on all `innerBlocks`. This value will be stored in the Block editor store and set to a block. * * @param {function} select State function. * @param {string} clientId Unique block ID from block editor. * @param {object} attributesObject Object of attributes to apply. * @param {array} exclude Array of block names to exclude. * * @access public * * @returns {void} * * Usage: * ```js * import { useSelect } from '@wordpress/data'; * * useSelect((select) => { * overrideInnerBlockAttributes( * select, * props.clientId, * { * wrapperUse: false, * wrapperNoControls: true, * } * ); * }); * ``` */ export const overrideInnerBlockAttributes = (select, clientId, attributesObject = {}, exclude = []) => { const { getBlock } = select('core/block-editor'); const block = getBlock(clientId); block.innerBlocks.map((item) => { const { attributes, name, } = item; if (!exclude.includes(name)) { for (const attribute in attributesObject) { if (Object.prototype.hasOwnProperty.call(attributesObject, attribute)) { if (attribute !== attributes[attribute]) { attributes[attribute] = attributesObject[attribute]; } } } } return item; }); }; /** * Sets attributes on all `innerBlocks`, with Simple wrapper options preset. This value will be stored in the Block editor store and set to a block. * * @param {function} select State function. * @param {string} clientId Unique block ID from block editor. * @param {array} exclude Array of block names to exclude. * * @access public * * @returns {void} * * Usage: * ```js * import { useSelect } from '@wordpress/data'; * * useSelect((select) => { * overrideInnerBlockSimpleWrapperAttributes( * select, * props.clientId * ); * }); * ``` */ export const overrideInnerBlockSimpleWrapperAttributes = (select, clientId, exclude = []) => { overrideInnerBlockAttributes( select, clientId, { wrapperSimple: true, wrapperSimpleShowControl: false, wrapperUseShowControl: false, }, exclude ); }; /** * Check if attribute exist in attributes list and add default value if not. * This is used because Block editor will not output attributes that don't have default value. * * @param {string} key - Key to check. * @param {array} attributes - Array of attributes. * @param {object} manifest - Components/blocks manifest.json * @param {boolean} [undefinedAllowed=false] - Allowed detection of undefined values. * * @access public * * @return {mixed} Based on the attribute type. * Boolean - false * String - '' * Object - {} * Array - [] * * Manifest: * ```js * { * "attributes": { * "buttonUse": { * "type": "boolean" * }, * }, * "buttonContent": { * "type": "string" * }, * } * } * ``` * * Usage: * ```js * checkAttr('buttonUse', attributes, manifest); * checkAttr('buttonContent', attributes, manifest); * ``` * * Output: * ```js * false * '' * ``` */ export const checkAttr = (key, attributes, manifest, undefinedAllowed = false) => { // Get the correct key for the check in the attributes object. const newKey = getAttrKey(key, attributes, manifest); // If key exists in the attributes object, just return that key value. if (Object.prototype.hasOwnProperty.call(attributes, newKey)) { return attributes[newKey]; } // Check current component attributes. const manifestKey = manifest.attributes[key]; let tipOutput = ''; if ('components' in manifest) { tipOutput = ' If you are using additional components, check if you used the correct block/component prefix in your attribute name.'; } // Bailout if key is missing. if (typeof manifestKey === 'undefined') { if ('blockName' in manifest) { throw Error(`${key} key does not exist in the ${manifest.blockName} block manifest. Please check your implementation.${tipOutput}`); } else { throw Error(`${key} key does not exist in the ${manifest.componentName} component manifest. Please check your implementation.${tipOutput}`); } } // If undefinedAllowed is true and attribute is missing default just return undefined to be able to unset attribute in block editor. if (!Object.prototype.hasOwnProperty.call(manifestKey, 'default') && undefinedAllowed) { return undefined; } // Check type. const defaultType = manifestKey.type; let defaultValue; // Output "default values" if none are defined. switch (defaultType) { case 'boolean': defaultValue = Object.prototype.hasOwnProperty.call(manifestKey, 'default') ? manifestKey.default : false; break; case 'array': defaultValue = Object.prototype.hasOwnProperty.call(manifestKey, 'default') ? manifestKey.default : []; break; case 'object': defaultValue = Object.prototype.hasOwnProperty.call(manifestKey, 'default') ? manifestKey.default : {}; break; default: defaultValue = Object.prototype.hasOwnProperty.call(manifestKey, 'default') ? manifestKey.default : ''; break; } return defaultValue; }; /** * Maps and check attributes for a responsive object using the checkAttr helper. * * @param {string} keyName - Key name to find in responsiveAttributes object. * @param {array} attributes - Array of attributes. * @param {object} manifest - Components/blocks manifest.json * @param {boolean} [undefinedAllowed=false] - Allowed detection of undefined values. * * @access public * * @returns {mixed} * * Manifest: * ```js * { * "attributes": { * "headingContentSpacingLarge": { * "type": "integer", * "default": 10, * }, * "headingContentSpacingDesktop": { * "type": "integer", * "default": 5, * }, * "headingContentSpacingTablet": { * "type": "integer", * "default": 3, * }, * "headingContentSpacingMobile": { * "type": "integer", * "default": 1, * } * }, * "responsiveAttributes": { * "headingContentSpacing": { * "large": "headingContentSpacingLarge", * "desktop": "headingContentSpacingDesktop", * "tablet": "headingContentSpacingTablet", * "mobile": "headingContentSpacingMobile" * } * } * } * ``` * * Usage: * ```js * checkAttrResponsive('headingContentSpacing', attributes, manifest); * ``` * * Output: * ```js * [ * large: 10, * desktop: 5, * tablet: 3, * mobile: 1, * ] * ``` */ export const checkAttrResponsive = (keyName, attributes, manifest, undefinedAllowed = false) => { const output = {}; // Bailout if missing keys. const responsiveAttributes = manifest?.responsiveAttributes; if (typeof responsiveAttributes === 'undefined') { if (typeof manifest['blockName'] === 'undefined') { throw Error(`It looks like you are missing responsiveAttributes key in your ${manifest['blockName']} block manifest.`); } else { throw Error(`It looks like you are missing responsiveAttributes key in your ${manifest['componentName']} component manifest.`); } } // Bailout if attribute keys is missing. if (!has(responsiveAttributes, keyName)) { throw Error(`It looks like you are missing ${keyName} key in your manifest responsiveAttributes object.`); } // Iterate keys in responsiveAttributes object and use checkAttr helper. for (const [key, value] of Object.entries(responsiveAttributes[keyName])) { output[key] = checkAttr(value, attributes, manifest, undefinedAllowed); } return output; }; /** * Check if attributes key has prefix and outputs the correct attribute name. * * @param {string} key - Key to check. * @param {array} attributes - Array of attributes. * @param {object} manifest - Components/blocks manifest.json * * @access public * * @return string */ export const getAttrKey = (key, attributes, manifest) => { // Just skip if attribute is wrapper. if (key.includes('wrapper')) { return key; } // Skip if using this helper in block. if (typeof manifest?.blockName !== 'undefined') { return key; } // If missing prefix or prefix is empty return key. if (typeof attributes?.prefix === 'undefined' || attributes?.prefix === '') { return key; } // No need to test if this is block or component because on top level block there is no prefix. // If there is a prefix, remove the attribute component name prefix and replace it with the new prefix. return key.replace(camelCase(manifest.componentName), attributes.prefix); }; /** * Output only attributes that are used in the component and remove everything else. * * @param {string} newName - *New* key to use to rename attributes. * @param {object} attributes - Attributes from the block/component. * @param {object} [manual={}] - Object of attributes to change key and merge to the original output. * * @access public * * @returns {object} * * Manifest: * ```js * const attributes = { * buttonColor: 'red', * buttonSize: 'big', * buttonIcon: 'blue', * blockName: 'button', * wrapperSize: 'big', * wrapperType: 'normal', * }; * ``` * * Usage: * ```js * {...props('button', attributes)} * ``` * * Output: * ```js * { * buttonColor: 'red', * buttonSize: 'big', * buttonIcon: 'blue', * blockName: 'button', * }; * ``` * * Additional keys that are passed are defined in the includes array. */ export const props = (newName, attributes, manual = {}) => { const output = {}; // Check which attributes we need to include. const includes = [ 'blockName', 'blockClientId', 'blockTopLevelId', 'blockFullName', 'blockClass', 'blockJsClass', 'componentJsClass', 'selectorClass', 'additionalClass', 'setAttributes', 'uniqueWrapperId', 'options', 'clientId', ]; // Check if in test mode and use different setting. const blockName = process.env.NODE_ENV === 'test' ? attributes.blockName.default : attributes.blockName; // Populate prefix key for recursive checks of attribute names. const prefix = (typeof attributes.prefix === 'undefined') ? camelCase(blockName) : attributes['prefix']; // Set component prefix. if (prefix === '') { output['prefix'] = camelCase(newName); } else { output['prefix'] = `${prefix}${upperFirst(camelCase(newName))}`; } // Iterate over attributes. for (const [key, value] of Object.entries(attributes)) { // Includes attributes from iteration. if (includes.includes(key)) { Object.assign(output, { [key]: value }); continue; } // If attribute starts with the prefix key leave it in the object if not remove it. if (key.startsWith(output['prefix'])) { Object.assign(output, { [key]: value }); } } // Check if you have manual object and prepare the attribute keys and merge them with the original attributes for output. if (!isEmpty(manual)) { // Iterate manual attributes. for (let [key, value] of Object.entries(manual)) { // Includes attributes from iteration. if (includes.includes(key)) { Object.assign(output, { [key]: value }); continue; } // Remove the current component name from the attribute name. const newKey = key.replace(`${lowerFirst(camelCase(newName))}`, ''); // Remove the old key. delete manual[key]; // // Add new key to the output with prepared attribute name. Object.assign(manual, { [`${output['prefix']}${newKey}`]: value }); } // Merge manual and output objects to one. Object.assign(output, manual); } // Return the original attribute for optimization purposes. return output; };