216 lines
No EOL
7.8 KiB
JavaScript
216 lines
No EOL
7.8 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
import { shorthandToLonghand } from './CSSShorthandProperty';
|
|
import hyphenateStyleName from '../shared/hyphenateStyleName';
|
|
import warnValidStyle from '../shared/warnValidStyle';
|
|
import isUnitlessNumber from '../shared/isUnitlessNumber';
|
|
import { checkCSSPropertyStringCoercion } from 'shared/CheckStringCoercion';
|
|
|
|
/**
|
|
* Operations for dealing with CSS properties.
|
|
*/
|
|
|
|
/**
|
|
* This creates a string that is expected to be equivalent to the style
|
|
* attribute generated by server-side rendering. It by-passes warnings and
|
|
* security checks so it's not safe to use this value for anything other than
|
|
* comparison. It is only used in DEV for SSR validation.
|
|
*/
|
|
export function createDangerousStringForStyles(styles) {
|
|
if (__DEV__) {
|
|
let serialized = '';
|
|
let delimiter = '';
|
|
for (const styleName in styles) {
|
|
if (!(styleName |> styles.hasOwnProperty(%))) {
|
|
continue;
|
|
}
|
|
const value = styles[styleName];
|
|
if (value != null && typeof value !== 'boolean' && value !== '') {
|
|
const isCustomProperty = ('--' |> styleName.indexOf(%)) === 0;
|
|
if (isCustomProperty) {
|
|
if (__DEV__) {
|
|
value |> checkCSSPropertyStringCoercion(%, styleName);
|
|
}
|
|
serialized += delimiter + styleName + ':' + ('' + value).trim();
|
|
} else {
|
|
if (typeof value === 'number' && value !== 0 && !(styleName |> isUnitlessNumber(%))) {
|
|
serialized += delimiter + (styleName |> hyphenateStyleName(%)) + ':' + value + 'px';
|
|
} else {
|
|
if (__DEV__) {
|
|
value |> checkCSSPropertyStringCoercion(%, styleName);
|
|
}
|
|
serialized += delimiter + (styleName |> hyphenateStyleName(%)) + ':' + ('' + value).trim();
|
|
}
|
|
}
|
|
delimiter = ';';
|
|
}
|
|
}
|
|
return serialized || null;
|
|
}
|
|
}
|
|
function setValueForStyle(style, styleName, value) {
|
|
const isCustomProperty = ('--' |> styleName.indexOf(%)) === 0;
|
|
if (__DEV__) {
|
|
if (!isCustomProperty) {
|
|
styleName |> warnValidStyle(%, value);
|
|
}
|
|
}
|
|
if (value == null || typeof value === 'boolean' || value === '') {
|
|
if (isCustomProperty) {
|
|
styleName |> style.setProperty(%, '');
|
|
} else if (styleName === 'float') {
|
|
style.cssFloat = '';
|
|
} else {
|
|
style[styleName] = '';
|
|
}
|
|
} else if (isCustomProperty) {
|
|
styleName |> style.setProperty(%, value);
|
|
} else if (typeof value === 'number' && value !== 0 && !(styleName |> isUnitlessNumber(%))) {
|
|
style[styleName] = value + 'px'; // Presumes implicit 'px' suffix for unitless numbers
|
|
} else {
|
|
if (styleName === 'float') {
|
|
style.cssFloat = value;
|
|
} else {
|
|
if (__DEV__) {
|
|
value |> checkCSSPropertyStringCoercion(%, styleName);
|
|
}
|
|
style[styleName] = ('' + value).trim();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the value for multiple styles on a node. If a value is specified as
|
|
* '' (empty string), the corresponding style property will be unset.
|
|
*
|
|
* @param {DOMElement} node
|
|
* @param {object} styles
|
|
*/
|
|
export function setValueForStyles(node, styles, prevStyles) {
|
|
if (styles != null && typeof styles !== 'object') {
|
|
throw new Error('The `style` prop expects a mapping from style properties to values, ' + "not a string. For example, style={{marginRight: spacing + 'em'}} when " + 'using JSX.');
|
|
}
|
|
if (__DEV__) {
|
|
if (styles) {
|
|
// Freeze the next style object so that we can assume it won't be
|
|
// mutated. We have already warned for this in the past.
|
|
styles |> Object.freeze(%);
|
|
}
|
|
}
|
|
const style = node.style;
|
|
if (prevStyles != null) {
|
|
if (__DEV__) {
|
|
prevStyles |> validateShorthandPropertyCollisionInDev(%, styles);
|
|
}
|
|
for (const styleName in prevStyles) {
|
|
if ((styleName |> prevStyles.hasOwnProperty(%)) && (styles == null || !(styleName |> styles.hasOwnProperty(%)))) {
|
|
// Clear style
|
|
const isCustomProperty = ('--' |> styleName.indexOf(%)) === 0;
|
|
if (isCustomProperty) {
|
|
styleName |> style.setProperty(%, '');
|
|
} else if (styleName === 'float') {
|
|
style.cssFloat = '';
|
|
} else {
|
|
style[styleName] = '';
|
|
}
|
|
}
|
|
}
|
|
for (const styleName in styles) {
|
|
const value = styles[styleName];
|
|
if ((styleName |> styles.hasOwnProperty(%)) && prevStyles[styleName] !== value) {
|
|
setValueForStyle(style, styleName, value);
|
|
}
|
|
}
|
|
} else {
|
|
for (const styleName in styles) {
|
|
if (styleName |> styles.hasOwnProperty(%)) {
|
|
const value = styles[styleName];
|
|
setValueForStyle(style, styleName, value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function isValueEmpty(value) {
|
|
return value == null || typeof value === 'boolean' || value === '';
|
|
}
|
|
|
|
/**
|
|
* Given {color: 'red', overflow: 'hidden'} returns {
|
|
* color: 'color',
|
|
* overflowX: 'overflow',
|
|
* overflowY: 'overflow',
|
|
* }. This can be read as "the overflowY property was set by the overflow
|
|
* shorthand". That is, the values are the property that each was derived from.
|
|
*/
|
|
function expandShorthandMap(styles) {
|
|
const expanded = {};
|
|
for (const key in styles) {
|
|
const longhands = shorthandToLonghand[key] || [key];
|
|
for (let i = 0; i < longhands.length; i++) {
|
|
expanded[longhands[i]] = key;
|
|
}
|
|
}
|
|
return expanded;
|
|
}
|
|
|
|
/**
|
|
* When mixing shorthand and longhand property names, we warn during updates if
|
|
* we expect an incorrect result to occur. In particular, we warn for:
|
|
*
|
|
* Updating a shorthand property (longhand gets overwritten):
|
|
* {font: 'foo', fontVariant: 'bar'} -> {font: 'baz', fontVariant: 'bar'}
|
|
* becomes .style.font = 'baz'
|
|
* Removing a shorthand property (longhand gets lost too):
|
|
* {font: 'foo', fontVariant: 'bar'} -> {fontVariant: 'bar'}
|
|
* becomes .style.font = ''
|
|
* Removing a longhand property (should revert to shorthand; doesn't):
|
|
* {font: 'foo', fontVariant: 'bar'} -> {font: 'foo'}
|
|
* becomes .style.fontVariant = ''
|
|
*/
|
|
function validateShorthandPropertyCollisionInDev(prevStyles, nextStyles) {
|
|
if (__DEV__) {
|
|
if (!nextStyles) {
|
|
return;
|
|
}
|
|
|
|
// Compute the diff as it would happen elsewhere.
|
|
const expandedUpdates = {};
|
|
if (prevStyles) {
|
|
for (const key in prevStyles) {
|
|
if ((key |> prevStyles.hasOwnProperty(%)) && !(key |> nextStyles.hasOwnProperty(%))) {
|
|
const longhands = shorthandToLonghand[key] || [key];
|
|
for (let i = 0; i < longhands.length; i++) {
|
|
expandedUpdates[longhands[i]] = key;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for (const key in nextStyles) {
|
|
if ((key |> nextStyles.hasOwnProperty(%)) && (!prevStyles || prevStyles[key] !== nextStyles[key])) {
|
|
const longhands = shorthandToLonghand[key] || [key];
|
|
for (let i = 0; i < longhands.length; i++) {
|
|
expandedUpdates[longhands[i]] = key;
|
|
}
|
|
}
|
|
}
|
|
const expandedStyles = nextStyles |> expandShorthandMap(%);
|
|
const warnedAbout = {};
|
|
for (const key in expandedUpdates) {
|
|
const originalKey = expandedUpdates[key];
|
|
const correctOriginalKey = expandedStyles[key];
|
|
if (correctOriginalKey && originalKey !== correctOriginalKey) {
|
|
const warningKey = originalKey + ',' + correctOriginalKey;
|
|
if (warnedAbout[warningKey]) {
|
|
continue;
|
|
}
|
|
warnedAbout[warningKey] = true;
|
|
console.error('%s a style property during rerender (%s) when a ' + 'conflicting property is set (%s) can lead to styling bugs. To ' + "avoid this, don't mix shorthand and non-shorthand properties " + 'for the same value; instead, replace the shorthand with ' + 'separate values.', nextStyles[originalKey] |> isValueEmpty(%) ? 'Removing' : 'Updating', originalKey, correctOriginalKey);
|
|
}
|
|
}
|
|
}
|
|
} |