mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
538 lines
14 KiB
JavaScript
538 lines
14 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
import warning from 'shared/warning';
|
|
|
|
type PropertyType = 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
|
|
// A reserved attribute.
|
|
// It is handled by React separately and shouldn't be written to the DOM.
|
|
export const RESERVED = 0;
|
|
|
|
// A simple string attribute.
|
|
// Attributes that aren't in the whitelist are presumed to have this type.
|
|
export const STRING = 1;
|
|
|
|
// A string attribute that accepts booleans in React. In HTML, these are called
|
|
// "enumerated" attributes with "true" and "false" as possible values.
|
|
// When true, it should be set to a "true" string.
|
|
// When false, it should be set to a "false" string.
|
|
export const BOOLEANISH_STRING = 2;
|
|
|
|
// A real boolean attribute.
|
|
// When true, it should be present (set either to an empty string or its name).
|
|
// When false, it should be omitted.
|
|
export const BOOLEAN = 3;
|
|
|
|
// An attribute that can be used as a flag as well as with a value.
|
|
// When true, it should be present (set either to an empty string or its name).
|
|
// When false, it should be omitted.
|
|
// For any other value, should be present with that value.
|
|
export const OVERLOADED_BOOLEAN = 4;
|
|
|
|
// An attribute that must be numeric or parse as a numeric.
|
|
// When falsy, it should be removed.
|
|
export const NUMERIC = 5;
|
|
|
|
// An attribute that must be positive numeric or parse as a positive numeric.
|
|
// When falsy, it should be removed.
|
|
export const POSITIVE_NUMERIC = 6;
|
|
|
|
export type PropertyInfo = {|
|
|
+acceptsBooleans: boolean,
|
|
+attributeName: string,
|
|
+attributeNamespace: string | null,
|
|
+mustUseProperty: boolean,
|
|
+propertyName: string,
|
|
+type: PropertyType,
|
|
|};
|
|
|
|
/* eslint-disable max-len */
|
|
export const ATTRIBUTE_NAME_START_CHAR =
|
|
':A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD';
|
|
/* eslint-enable max-len */
|
|
export const ATTRIBUTE_NAME_CHAR =
|
|
ATTRIBUTE_NAME_START_CHAR + '\\-.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040';
|
|
|
|
export const ID_ATTRIBUTE_NAME = 'data-reactid';
|
|
export const ROOT_ATTRIBUTE_NAME = 'data-reactroot';
|
|
export const VALID_ATTRIBUTE_NAME_REGEX = new RegExp(
|
|
'^[' + ATTRIBUTE_NAME_START_CHAR + '][' + ATTRIBUTE_NAME_CHAR + ']*$',
|
|
);
|
|
|
|
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
|
const illegalAttributeNameCache = {};
|
|
const validatedAttributeNameCache = {};
|
|
|
|
export function isAttributeNameSafe(attributeName: string): boolean {
|
|
if (hasOwnProperty.call(validatedAttributeNameCache, attributeName)) {
|
|
return true;
|
|
}
|
|
if (hasOwnProperty.call(illegalAttributeNameCache, attributeName)) {
|
|
return false;
|
|
}
|
|
if (VALID_ATTRIBUTE_NAME_REGEX.test(attributeName)) {
|
|
validatedAttributeNameCache[attributeName] = true;
|
|
return true;
|
|
}
|
|
illegalAttributeNameCache[attributeName] = true;
|
|
if (__DEV__) {
|
|
warning(false, 'Invalid attribute name: `%s`', attributeName);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
export function shouldIgnoreAttribute(
|
|
name: string,
|
|
propertyInfo: PropertyInfo | null,
|
|
isCustomComponentTag: boolean,
|
|
): boolean {
|
|
if (propertyInfo !== null) {
|
|
return propertyInfo.type === RESERVED;
|
|
}
|
|
if (isCustomComponentTag) {
|
|
return false;
|
|
}
|
|
if (
|
|
name.length > 2 &&
|
|
(name[0] === 'o' || name[0] === 'O') &&
|
|
(name[1] === 'n' || name[1] === 'N')
|
|
) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
export function shouldRemoveAttributeWithWarning(
|
|
name: string,
|
|
value: mixed,
|
|
propertyInfo: PropertyInfo | null,
|
|
isCustomComponentTag: boolean,
|
|
): boolean {
|
|
if (propertyInfo !== null && propertyInfo.type === RESERVED) {
|
|
return false;
|
|
}
|
|
switch (typeof value) {
|
|
case 'function':
|
|
// $FlowIssue symbol is perfectly valid here
|
|
case 'symbol': // eslint-disable-line
|
|
return true;
|
|
case 'boolean': {
|
|
if (isCustomComponentTag) {
|
|
return false;
|
|
}
|
|
if (propertyInfo !== null) {
|
|
return !propertyInfo.acceptsBooleans;
|
|
} else {
|
|
const prefix = name.toLowerCase().slice(0, 5);
|
|
return prefix !== 'data-' && prefix !== 'aria-';
|
|
}
|
|
}
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export function shouldRemoveAttribute(
|
|
name: string,
|
|
value: mixed,
|
|
propertyInfo: PropertyInfo | null,
|
|
isCustomComponentTag: boolean,
|
|
): boolean {
|
|
if (value === null || typeof value === 'undefined') {
|
|
return true;
|
|
}
|
|
if (
|
|
shouldRemoveAttributeWithWarning(
|
|
name,
|
|
value,
|
|
propertyInfo,
|
|
isCustomComponentTag,
|
|
)
|
|
) {
|
|
return true;
|
|
}
|
|
if (isCustomComponentTag) {
|
|
return false;
|
|
}
|
|
if (propertyInfo !== null) {
|
|
switch (propertyInfo.type) {
|
|
case BOOLEAN:
|
|
return !value;
|
|
case OVERLOADED_BOOLEAN:
|
|
return value === false;
|
|
case NUMERIC:
|
|
return isNaN(value);
|
|
case POSITIVE_NUMERIC:
|
|
return isNaN(value) || (value: any) < 1;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
export function getPropertyInfo(name: string): PropertyInfo | null {
|
|
return properties.hasOwnProperty(name) ? properties[name] : null;
|
|
}
|
|
|
|
function PropertyInfoRecord(
|
|
name: string,
|
|
type: PropertyType,
|
|
mustUseProperty: boolean,
|
|
attributeName: string,
|
|
attributeNamespace: string | null,
|
|
) {
|
|
this.acceptsBooleans =
|
|
type === BOOLEANISH_STRING ||
|
|
type === BOOLEAN ||
|
|
type === OVERLOADED_BOOLEAN;
|
|
this.attributeName = attributeName;
|
|
this.attributeNamespace = attributeNamespace;
|
|
this.mustUseProperty = mustUseProperty;
|
|
this.propertyName = name;
|
|
this.type = type;
|
|
}
|
|
|
|
// When adding attributes to this list, be sure to also add them to
|
|
// the `possibleStandardNames` module to ensure casing and incorrect
|
|
// name warnings.
|
|
const properties = {};
|
|
|
|
// These props are reserved by React. They shouldn't be written to the DOM.
|
|
[
|
|
'children',
|
|
'dangerouslySetInnerHTML',
|
|
// TODO: This prevents the assignment of defaultValue to regular
|
|
// elements (not just inputs). Now that ReactDOMInput assigns to the
|
|
// defaultValue property -- do we need this?
|
|
'defaultValue',
|
|
'defaultChecked',
|
|
'innerHTML',
|
|
'suppressContentEditableWarning',
|
|
'suppressHydrationWarning',
|
|
'style',
|
|
].forEach(name => {
|
|
properties[name] = new PropertyInfoRecord(
|
|
name,
|
|
RESERVED,
|
|
false, // mustUseProperty
|
|
name, // attributeName
|
|
null, // attributeNamespace
|
|
);
|
|
});
|
|
|
|
// A few React string attributes have a different name.
|
|
// This is a mapping from React prop names to the attribute names.
|
|
[
|
|
['acceptCharset', 'accept-charset'],
|
|
['className', 'class'],
|
|
['htmlFor', 'for'],
|
|
['httpEquiv', 'http-equiv'],
|
|
].forEach(([name, attributeName]) => {
|
|
properties[name] = new PropertyInfoRecord(
|
|
name,
|
|
STRING,
|
|
false, // mustUseProperty
|
|
attributeName, // attributeName
|
|
null, // attributeNamespace
|
|
);
|
|
});
|
|
|
|
// These are "enumerated" HTML attributes that accept "true" and "false".
|
|
// In React, we let users pass `true` and `false` even though technically
|
|
// these aren't boolean attributes (they are coerced to strings).
|
|
['contentEditable', 'draggable', 'spellCheck', 'value'].forEach(name => {
|
|
properties[name] = new PropertyInfoRecord(
|
|
name,
|
|
BOOLEANISH_STRING,
|
|
false, // mustUseProperty
|
|
name.toLowerCase(), // attributeName
|
|
null, // attributeNamespace
|
|
);
|
|
});
|
|
|
|
// These are "enumerated" SVG attributes that accept "true" and "false".
|
|
// In React, we let users pass `true` and `false` even though technically
|
|
// these aren't boolean attributes (they are coerced to strings).
|
|
// Since these are SVG attributes, their attribute names are case-sensitive.
|
|
[
|
|
'autoReverse',
|
|
'externalResourcesRequired',
|
|
'focusable',
|
|
'preserveAlpha',
|
|
].forEach(name => {
|
|
properties[name] = new PropertyInfoRecord(
|
|
name,
|
|
BOOLEANISH_STRING,
|
|
false, // mustUseProperty
|
|
name, // attributeName
|
|
null, // attributeNamespace
|
|
);
|
|
});
|
|
|
|
// These are HTML boolean attributes.
|
|
[
|
|
'allowFullScreen',
|
|
'async',
|
|
// Note: there is a special case that prevents it from being written to the DOM
|
|
// on the client side because the browsers are inconsistent. Instead we call focus().
|
|
'autoFocus',
|
|
'autoPlay',
|
|
'controls',
|
|
'default',
|
|
'defer',
|
|
'disabled',
|
|
'formNoValidate',
|
|
'hidden',
|
|
'loop',
|
|
'noModule',
|
|
'noValidate',
|
|
'open',
|
|
'playsInline',
|
|
'readOnly',
|
|
'required',
|
|
'reversed',
|
|
'scoped',
|
|
'seamless',
|
|
// Microdata
|
|
'itemScope',
|
|
].forEach(name => {
|
|
properties[name] = new PropertyInfoRecord(
|
|
name,
|
|
BOOLEAN,
|
|
false, // mustUseProperty
|
|
name.toLowerCase(), // attributeName
|
|
null, // attributeNamespace
|
|
);
|
|
});
|
|
|
|
// These are the few React props that we set as DOM properties
|
|
// rather than attributes. These are all booleans.
|
|
[
|
|
'checked',
|
|
// Note: `option.selected` is not updated if `select.multiple` is
|
|
// disabled with `removeAttribute`. We have special logic for handling this.
|
|
'multiple',
|
|
'muted',
|
|
'selected',
|
|
|
|
// NOTE: if you add a camelCased prop to this list,
|
|
// you'll need to set attributeName to name.toLowerCase()
|
|
// instead in the assignment below.
|
|
].forEach(name => {
|
|
properties[name] = new PropertyInfoRecord(
|
|
name,
|
|
BOOLEAN,
|
|
true, // mustUseProperty
|
|
name, // attributeName
|
|
null, // attributeNamespace
|
|
);
|
|
});
|
|
|
|
// These are HTML attributes that are "overloaded booleans": they behave like
|
|
// booleans, but can also accept a string value.
|
|
[
|
|
'capture',
|
|
'download',
|
|
|
|
// NOTE: if you add a camelCased prop to this list,
|
|
// you'll need to set attributeName to name.toLowerCase()
|
|
// instead in the assignment below.
|
|
].forEach(name => {
|
|
properties[name] = new PropertyInfoRecord(
|
|
name,
|
|
OVERLOADED_BOOLEAN,
|
|
false, // mustUseProperty
|
|
name, // attributeName
|
|
null, // attributeNamespace
|
|
);
|
|
});
|
|
|
|
// These are HTML attributes that must be positive numbers.
|
|
[
|
|
'cols',
|
|
'rows',
|
|
'size',
|
|
'span',
|
|
|
|
// NOTE: if you add a camelCased prop to this list,
|
|
// you'll need to set attributeName to name.toLowerCase()
|
|
// instead in the assignment below.
|
|
].forEach(name => {
|
|
properties[name] = new PropertyInfoRecord(
|
|
name,
|
|
POSITIVE_NUMERIC,
|
|
false, // mustUseProperty
|
|
name, // attributeName
|
|
null, // attributeNamespace
|
|
);
|
|
});
|
|
|
|
// These are HTML attributes that must be numbers.
|
|
['rowSpan', 'start'].forEach(name => {
|
|
properties[name] = new PropertyInfoRecord(
|
|
name,
|
|
NUMERIC,
|
|
false, // mustUseProperty
|
|
name.toLowerCase(), // attributeName
|
|
null, // attributeNamespace
|
|
);
|
|
});
|
|
|
|
const CAMELIZE = /[\-\:]([a-z])/g;
|
|
const capitalize = token => token[1].toUpperCase();
|
|
|
|
// This is a list of all SVG attributes that need special casing, namespacing,
|
|
// or boolean value assignment. Regular attributes that just accept strings
|
|
// and have the same names are omitted, just like in the HTML whitelist.
|
|
// Some of these attributes can be hard to find. This list was created by
|
|
// scrapping the MDN documentation.
|
|
[
|
|
'accent-height',
|
|
'alignment-baseline',
|
|
'arabic-form',
|
|
'baseline-shift',
|
|
'cap-height',
|
|
'clip-path',
|
|
'clip-rule',
|
|
'color-interpolation',
|
|
'color-interpolation-filters',
|
|
'color-profile',
|
|
'color-rendering',
|
|
'dominant-baseline',
|
|
'enable-background',
|
|
'fill-opacity',
|
|
'fill-rule',
|
|
'flood-color',
|
|
'flood-opacity',
|
|
'font-family',
|
|
'font-size',
|
|
'font-size-adjust',
|
|
'font-stretch',
|
|
'font-style',
|
|
'font-variant',
|
|
'font-weight',
|
|
'glyph-name',
|
|
'glyph-orientation-horizontal',
|
|
'glyph-orientation-vertical',
|
|
'horiz-adv-x',
|
|
'horiz-origin-x',
|
|
'image-rendering',
|
|
'letter-spacing',
|
|
'lighting-color',
|
|
'marker-end',
|
|
'marker-mid',
|
|
'marker-start',
|
|
'overline-position',
|
|
'overline-thickness',
|
|
'paint-order',
|
|
'panose-1',
|
|
'pointer-events',
|
|
'rendering-intent',
|
|
'shape-rendering',
|
|
'stop-color',
|
|
'stop-opacity',
|
|
'strikethrough-position',
|
|
'strikethrough-thickness',
|
|
'stroke-dasharray',
|
|
'stroke-dashoffset',
|
|
'stroke-linecap',
|
|
'stroke-linejoin',
|
|
'stroke-miterlimit',
|
|
'stroke-opacity',
|
|
'stroke-width',
|
|
'text-anchor',
|
|
'text-decoration',
|
|
'text-rendering',
|
|
'underline-position',
|
|
'underline-thickness',
|
|
'unicode-bidi',
|
|
'unicode-range',
|
|
'units-per-em',
|
|
'v-alphabetic',
|
|
'v-hanging',
|
|
'v-ideographic',
|
|
'v-mathematical',
|
|
'vector-effect',
|
|
'vert-adv-y',
|
|
'vert-origin-x',
|
|
'vert-origin-y',
|
|
'word-spacing',
|
|
'writing-mode',
|
|
'xmlns:xlink',
|
|
'x-height',
|
|
|
|
// NOTE: if you add a camelCased prop to this list,
|
|
// you'll need to set attributeName to name.toLowerCase()
|
|
// instead in the assignment below.
|
|
].forEach(attributeName => {
|
|
const name = attributeName.replace(CAMELIZE, capitalize);
|
|
properties[name] = new PropertyInfoRecord(
|
|
name,
|
|
STRING,
|
|
false, // mustUseProperty
|
|
attributeName,
|
|
null, // attributeNamespace
|
|
);
|
|
});
|
|
|
|
// String SVG attributes with the xlink namespace.
|
|
[
|
|
'xlink:actuate',
|
|
'xlink:arcrole',
|
|
'xlink:href',
|
|
'xlink:role',
|
|
'xlink:show',
|
|
'xlink:title',
|
|
'xlink:type',
|
|
|
|
// NOTE: if you add a camelCased prop to this list,
|
|
// you'll need to set attributeName to name.toLowerCase()
|
|
// instead in the assignment below.
|
|
].forEach(attributeName => {
|
|
const name = attributeName.replace(CAMELIZE, capitalize);
|
|
properties[name] = new PropertyInfoRecord(
|
|
name,
|
|
STRING,
|
|
false, // mustUseProperty
|
|
attributeName,
|
|
'http://www.w3.org/1999/xlink',
|
|
);
|
|
});
|
|
|
|
// String SVG attributes with the xml namespace.
|
|
[
|
|
'xml:base',
|
|
'xml:lang',
|
|
'xml:space',
|
|
|
|
// NOTE: if you add a camelCased prop to this list,
|
|
// you'll need to set attributeName to name.toLowerCase()
|
|
// instead in the assignment below.
|
|
].forEach(attributeName => {
|
|
const name = attributeName.replace(CAMELIZE, capitalize);
|
|
properties[name] = new PropertyInfoRecord(
|
|
name,
|
|
STRING,
|
|
false, // mustUseProperty
|
|
attributeName,
|
|
'http://www.w3.org/XML/1998/namespace',
|
|
);
|
|
});
|
|
|
|
// Special case: this attribute exists both in HTML and SVG.
|
|
// Its "tabindex" attribute name is case-sensitive in SVG so we can't just use
|
|
// its React `tabIndex` name, like we do for attributes that exist only in HTML.
|
|
properties.tabIndex = new PropertyInfoRecord(
|
|
'tabIndex',
|
|
STRING,
|
|
false, // mustUseProperty
|
|
'tabindex', // attributeName
|
|
null, // attributeNamespace
|
|
);
|