1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  */
    
  7. 
    
  8. import {shorthandToLonghand} from './CSSShorthandProperty';
    
  9. 
    
  10. import hyphenateStyleName from '../shared/hyphenateStyleName';
    
  11. import warnValidStyle from '../shared/warnValidStyle';
    
  12. import isUnitlessNumber from '../shared/isUnitlessNumber';
    
  13. import {checkCSSPropertyStringCoercion} from 'shared/CheckStringCoercion';
    
  14. 
    
  15. /**
    
  16.  * Operations for dealing with CSS properties.
    
  17.  */
    
  18. 
    
  19. /**
    
  20.  * This creates a string that is expected to be equivalent to the style
    
  21.  * attribute generated by server-side rendering. It by-passes warnings and
    
  22.  * security checks so it's not safe to use this value for anything other than
    
  23.  * comparison. It is only used in DEV for SSR validation.
    
  24.  */
    
  25. export function createDangerousStringForStyles(styles) {
    
  26.   if (__DEV__) {
    
  27.     let serialized = '';
    
  28.     let delimiter = '';
    
  29.     for (const styleName in styles) {
    
  30.       if (!styles.hasOwnProperty(styleName)) {
    
  31.         continue;
    
  32.       }
    
  33.       const value = styles[styleName];
    
  34.       if (value != null && typeof value !== 'boolean' && value !== '') {
    
  35.         const isCustomProperty = styleName.indexOf('--') === 0;
    
  36.         if (isCustomProperty) {
    
  37.           if (__DEV__) {
    
  38.             checkCSSPropertyStringCoercion(value, styleName);
    
  39.           }
    
  40.           serialized += delimiter + styleName + ':' + ('' + value).trim();
    
  41.         } else {
    
  42.           if (
    
  43.             typeof value === 'number' &&
    
  44.             value !== 0 &&
    
  45.             !isUnitlessNumber(styleName)
    
  46.           ) {
    
  47.             serialized +=
    
  48.               delimiter + hyphenateStyleName(styleName) + ':' + value + 'px';
    
  49.           } else {
    
  50.             if (__DEV__) {
    
  51.               checkCSSPropertyStringCoercion(value, styleName);
    
  52.             }
    
  53.             serialized +=
    
  54.               delimiter +
    
  55.               hyphenateStyleName(styleName) +
    
  56.               ':' +
    
  57.               ('' + value).trim();
    
  58.           }
    
  59.         }
    
  60.         delimiter = ';';
    
  61.       }
    
  62.     }
    
  63.     return serialized || null;
    
  64.   }
    
  65. }
    
  66. 
    
  67. function setValueForStyle(style, styleName, value) {
    
  68.   const isCustomProperty = styleName.indexOf('--') === 0;
    
  69.   if (__DEV__) {
    
  70.     if (!isCustomProperty) {
    
  71.       warnValidStyle(styleName, value);
    
  72.     }
    
  73.   }
    
  74. 
    
  75.   if (value == null || typeof value === 'boolean' || value === '') {
    
  76.     if (isCustomProperty) {
    
  77.       style.setProperty(styleName, '');
    
  78.     } else if (styleName === 'float') {
    
  79.       style.cssFloat = '';
    
  80.     } else {
    
  81.       style[styleName] = '';
    
  82.     }
    
  83.   } else if (isCustomProperty) {
    
  84.     style.setProperty(styleName, value);
    
  85.   } else if (
    
  86.     typeof value === 'number' &&
    
  87.     value !== 0 &&
    
  88.     !isUnitlessNumber(styleName)
    
  89.   ) {
    
  90.     style[styleName] = value + 'px'; // Presumes implicit 'px' suffix for unitless numbers
    
  91.   } else {
    
  92.     if (styleName === 'float') {
    
  93.       style.cssFloat = value;
    
  94.     } else {
    
  95.       if (__DEV__) {
    
  96.         checkCSSPropertyStringCoercion(value, styleName);
    
  97.       }
    
  98.       style[styleName] = ('' + value).trim();
    
  99.     }
    
  100.   }
    
  101. }
    
  102. 
    
  103. /**
    
  104.  * Sets the value for multiple styles on a node.  If a value is specified as
    
  105.  * '' (empty string), the corresponding style property will be unset.
    
  106.  *
    
  107.  * @param {DOMElement} node
    
  108.  * @param {object} styles
    
  109.  */
    
  110. export function setValueForStyles(node, styles, prevStyles) {
    
  111.   if (styles != null && typeof styles !== 'object') {
    
  112.     throw new Error(
    
  113.       'The `style` prop expects a mapping from style properties to values, ' +
    
  114.         "not a string. For example, style={{marginRight: spacing + 'em'}} when " +
    
  115.         'using JSX.',
    
  116.     );
    
  117.   }
    
  118.   if (__DEV__) {
    
  119.     if (styles) {
    
  120.       // Freeze the next style object so that we can assume it won't be
    
  121.       // mutated. We have already warned for this in the past.
    
  122.       Object.freeze(styles);
    
  123.     }
    
  124.   }
    
  125. 
    
  126.   const style = node.style;
    
  127. 
    
  128.   if (prevStyles != null) {
    
  129.     if (__DEV__) {
    
  130.       validateShorthandPropertyCollisionInDev(prevStyles, styles);
    
  131.     }
    
  132. 
    
  133.     for (const styleName in prevStyles) {
    
  134.       if (
    
  135.         prevStyles.hasOwnProperty(styleName) &&
    
  136.         (styles == null || !styles.hasOwnProperty(styleName))
    
  137.       ) {
    
  138.         // Clear style
    
  139.         const isCustomProperty = styleName.indexOf('--') === 0;
    
  140.         if (isCustomProperty) {
    
  141.           style.setProperty(styleName, '');
    
  142.         } else if (styleName === 'float') {
    
  143.           style.cssFloat = '';
    
  144.         } else {
    
  145.           style[styleName] = '';
    
  146.         }
    
  147.       }
    
  148.     }
    
  149.     for (const styleName in styles) {
    
  150.       const value = styles[styleName];
    
  151.       if (styles.hasOwnProperty(styleName) && prevStyles[styleName] !== value) {
    
  152.         setValueForStyle(style, styleName, value);
    
  153.       }
    
  154.     }
    
  155.   } else {
    
  156.     for (const styleName in styles) {
    
  157.       if (styles.hasOwnProperty(styleName)) {
    
  158.         const value = styles[styleName];
    
  159.         setValueForStyle(style, styleName, value);
    
  160.       }
    
  161.     }
    
  162.   }
    
  163. }
    
  164. 
    
  165. function isValueEmpty(value) {
    
  166.   return value == null || typeof value === 'boolean' || value === '';
    
  167. }
    
  168. 
    
  169. /**
    
  170.  * Given {color: 'red', overflow: 'hidden'} returns {
    
  171.  *   color: 'color',
    
  172.  *   overflowX: 'overflow',
    
  173.  *   overflowY: 'overflow',
    
  174.  * }. This can be read as "the overflowY property was set by the overflow
    
  175.  * shorthand". That is, the values are the property that each was derived from.
    
  176.  */
    
  177. function expandShorthandMap(styles) {
    
  178.   const expanded = {};
    
  179.   for (const key in styles) {
    
  180.     const longhands = shorthandToLonghand[key] || [key];
    
  181.     for (let i = 0; i < longhands.length; i++) {
    
  182.       expanded[longhands[i]] = key;
    
  183.     }
    
  184.   }
    
  185.   return expanded;
    
  186. }
    
  187. 
    
  188. /**
    
  189.  * When mixing shorthand and longhand property names, we warn during updates if
    
  190.  * we expect an incorrect result to occur. In particular, we warn for:
    
  191.  *
    
  192.  * Updating a shorthand property (longhand gets overwritten):
    
  193.  *   {font: 'foo', fontVariant: 'bar'} -> {font: 'baz', fontVariant: 'bar'}
    
  194.  *   becomes .style.font = 'baz'
    
  195.  * Removing a shorthand property (longhand gets lost too):
    
  196.  *   {font: 'foo', fontVariant: 'bar'} -> {fontVariant: 'bar'}
    
  197.  *   becomes .style.font = ''
    
  198.  * Removing a longhand property (should revert to shorthand; doesn't):
    
  199.  *   {font: 'foo', fontVariant: 'bar'} -> {font: 'foo'}
    
  200.  *   becomes .style.fontVariant = ''
    
  201.  */
    
  202. function validateShorthandPropertyCollisionInDev(prevStyles, nextStyles) {
    
  203.   if (__DEV__) {
    
  204.     if (!nextStyles) {
    
  205.       return;
    
  206.     }
    
  207. 
    
  208.     // Compute the diff as it would happen elsewhere.
    
  209.     const expandedUpdates = {};
    
  210.     if (prevStyles) {
    
  211.       for (const key in prevStyles) {
    
  212.         if (prevStyles.hasOwnProperty(key) && !nextStyles.hasOwnProperty(key)) {
    
  213.           const longhands = shorthandToLonghand[key] || [key];
    
  214.           for (let i = 0; i < longhands.length; i++) {
    
  215.             expandedUpdates[longhands[i]] = key;
    
  216.           }
    
  217.         }
    
  218.       }
    
  219.     }
    
  220.     for (const key in nextStyles) {
    
  221.       if (
    
  222.         nextStyles.hasOwnProperty(key) &&
    
  223.         (!prevStyles || prevStyles[key] !== nextStyles[key])
    
  224.       ) {
    
  225.         const longhands = shorthandToLonghand[key] || [key];
    
  226.         for (let i = 0; i < longhands.length; i++) {
    
  227.           expandedUpdates[longhands[i]] = key;
    
  228.         }
    
  229.       }
    
  230.     }
    
  231. 
    
  232.     const expandedStyles = expandShorthandMap(nextStyles);
    
  233.     const warnedAbout = {};
    
  234.     for (const key in expandedUpdates) {
    
  235.       const originalKey = expandedUpdates[key];
    
  236.       const correctOriginalKey = expandedStyles[key];
    
  237.       if (correctOriginalKey && originalKey !== correctOriginalKey) {
    
  238.         const warningKey = originalKey + ',' + correctOriginalKey;
    
  239.         if (warnedAbout[warningKey]) {
    
  240.           continue;
    
  241.         }
    
  242.         warnedAbout[warningKey] = true;
    
  243.         console.error(
    
  244.           '%s a style property during rerender (%s) when a ' +
    
  245.             'conflicting property is set (%s) can lead to styling bugs. To ' +
    
  246.             "avoid this, don't mix shorthand and non-shorthand properties " +
    
  247.             'for the same value; instead, replace the shorthand with ' +
    
  248.             'separate values.',
    
  249.           isValueEmpty(nextStyles[originalKey]) ? 'Removing' : 'Updating',
    
  250.           originalKey,
    
  251.           correctOriginalKey,
    
  252.         );
    
  253.       }
    
  254.     }
    
  255.   }
    
  256. }