[change] StyleSheet refactor

* HTML class names are now hashes of the corresponding declaration
* Simplifies 'setNativeProps' logic
* Fixes use of server-rendered style sheet
* Fixes duplicate insertion of style sheets with hot-reloading

No significant change to the benchmark results
This commit is contained in:
Nicolas Gallagher
2017-02-27 15:59:18 -08:00
parent f391031fb1
commit 0361845537
16 changed files with 486 additions and 499 deletions
+127
View File
@@ -0,0 +1,127 @@
import asap from 'asap';
import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
import generateCss from './generateCss';
import hash from './hash';
import staticCss from './staticCss';
const emptyObject = {};
const STYLE_ELEMENT_ID = 'react-native-stylesheet';
const createClassName = (prop, value) => {
const hashed = hash(prop + value);
return process.env.NODE_ENV !== 'production' ? `rn-${prop}-${hashed}` : `rn-${hashed}`;
};
const createCssRule = (className, prop, value) => {
const css = generateCss({ [prop]: value });
const selector = `.${className}`;
return `${selector}{${css}}`;
};
const pointerEvents = {
auto: createClassName('pointerEvents', 'auto'),
boxNone: createClassName('pointerEvents', 'box-none'),
boxOnly: createClassName('pointerEvents', 'box-only'),
none: createClassName('pointerEvents', 'none')
};
const pointerEventsCss =
`.${pointerEvents.auto}{pointer-events:auto;}\n` +
`.${pointerEvents.boxNone}{pointer-events:none;}\n` +
`.${pointerEvents.boxNone} *{pointer-events:auto;}\n` +
`.${pointerEvents.boxOnly}{pointer-events:auto;}\n` +
`.${pointerEvents.boxOnly} *{pointer-events:none;}\n` +
`.${pointerEvents.none}{pointer-events:none;}`;
class StyleManager {
constructor() {
// custom pointer event values are implemented using descendent selectors,
// so we manually create the CSS and pre-register the declarations
const pointerEventsPropName = 'pointerEvents';
this.cache = {
byClassName: {
[pointerEvents.auto]: { prop: pointerEventsPropName, value: 'auto' },
[pointerEvents.boxNone]: { prop: pointerEventsPropName, value: 'box-none' },
[pointerEvents.boxOnly]: { prop: pointerEventsPropName, value: 'box-only' },
[pointerEvents.none]: { prop: pointerEventsPropName, value: 'none' }
},
byProp: {
pointerEvents: {
'auto': pointerEvents.auto,
'box-none': pointerEvents.boxNone,
'box-only': pointerEvents.boxOnly,
'none': pointerEvents.none
}
}
};
// on the client we check for an existing style sheet before injecting style sheets
if (canUseDOM) {
const prerenderedStyleSheet = document.getElementById(STYLE_ELEMENT_ID);
if (prerenderedStyleSheet) {
this.mainSheet = prerenderedStyleSheet;
} else {
document.head.insertAdjacentHTML('afterbegin', this.getStyleSheetHtml());
this.mainSheet = document.getElementById(STYLE_ELEMENT_ID);
}
}
}
getClassName(prop, value) {
const cache = this.cache.byProp;
return cache[prop] && cache[prop].hasOwnProperty(value) && cache[prop][value];
}
getDeclaration(className) {
const cache = this.cache.byClassName;
return cache[className] || emptyObject;
}
getStyleSheetHtml() {
const cache = this.cache.byProp;
const mainSheetTextContext = Object.keys(cache).reduce((rules, prop) => {
if (prop !== 'pointerEvents') {
Object.keys(cache[prop]).forEach((value) => {
const className = this.getClassName(prop, value);
rules.push(createCssRule(className, prop, value));
});
}
return rules;
}, []).join('\n');
const staticSheet = `<style id="react-native-stylesheet-static">\n${staticCss}\n${pointerEventsCss}\n</style>`;
const mainSheet = `<style id="${STYLE_ELEMENT_ID}">\n${mainSheetTextContext}\n</style>`;
return `${staticSheet}\n${mainSheet}`;
}
setDeclaration(prop, value) {
let className = this.getClassName(prop, value);
if (!className) {
className = createClassName(prop, value);
this._addToCache(className, prop, value);
if (canUseDOM) {
asap(() => {
const sheet = this.mainSheet.sheet;
// avoid injecting if the rule already exists (e.g., server rendered, hot reload)
if (this.mainSheet.textContent.indexOf(className) === -1) {
const rule = createCssRule(className, prop, value);
sheet.insertRule(rule, sheet.cssRules.length);
}
});
}
}
return className;
}
_addToCache(className, prop, value) {
const cache = this.cache;
if (!cache.byProp[prop]) {
cache.byProp[prop] = {};
}
cache.byProp[prop][value] = className;
cache.byClassName[className] = { prop, value };
}
}
module.exports = StyleManager;
+158
View File
@@ -0,0 +1,158 @@
/**
* WARNING: changes to this file in particular can cause significant changes to
* the results of render performance benchmarks.
*/
import createReactDOMStyle from './createReactDOMStyle';
import flattenArray from '../../modules/flattenArray';
import flattenStyle from './flattenStyle';
import I18nManager from '../I18nManager';
import mapKeyValue from '../../modules/mapKeyValue';
import prefixInlineStyles from './prefixInlineStyles';
import ReactNativePropRegistry from '../../modules/ReactNativePropRegistry';
import StyleManager from './StyleManager';
const createCacheKey = (id) => {
const prefix = I18nManager.isRTL ? 'rtl' : 'ltr';
return `${prefix}-${id}`;
};
const classListToString = (list) => list.join(' ').trim();
class StyleRegistry {
constructor() {
this.cache = {};
this.styleManager = new StyleManager();
}
getStyleSheetHtml() {
return this.styleManager.getStyleSheetHtml();
}
/**
* Registers and precaches a React Native style object to HTML class names
*/
register(flatStyle) {
const id = ReactNativePropRegistry.register(flatStyle);
const key = createCacheKey(id);
const style = createReactDOMStyle(flatStyle);
const classList = mapKeyValue(style, (prop, value) => {
if (value != null) {
return this.styleManager.setDeclaration(prop, value);
}
});
const className = classList.join(' ').trim();
this.cache[key] = { classList, className };
return id;
}
/**
* Resolves a React Native style object to DOM attributes
*/
resolve(reactNativeStyle) {
if (!reactNativeStyle) {
return undefined;
}
// fast and cachable
if (typeof reactNativeStyle === 'number') {
const key = createCacheKey(reactNativeStyle);
return this._resolveStyleIfNeeded(key, reactNativeStyle);
}
// resolve a plain RN style object
if (!Array.isArray(reactNativeStyle)) {
return this._resolveStyle(reactNativeStyle);
}
// flatten the style array
// cache resolved props when all styles are registered
// otherwise fallback to resolving
const flatArray = flattenArray(reactNativeStyle);
let isArrayOfNumbers = true;
for (let i = 0; i < flatArray.length; i++) {
if (typeof flatArray[i] !== 'number') {
isArrayOfNumbers = false;
break;
}
}
const key = isArrayOfNumbers ? createCacheKey(flatArray.join('-')) : null;
return this._resolveStyleIfNeeded(key, flatArray);
}
/**
* Resolves a React Native style object to DOM attributes, accounting for
* the existing styles applied to the DOM node
*/
resolveStateful(reactNativeStyle, domClassList) {
const previousReactNativeStyle = {};
const preservedClassNames = [];
// Convert the existing classList to a React Native style and preserve any
// unrecognized classNames.
domClassList.forEach((className) => {
const { prop, value } = this.styleManager.getDeclaration(className);
if (prop) {
previousReactNativeStyle[prop] = value;
} else {
preservedClassNames.push(className);
}
});
// Resolve the two React Native styles.
const { classList, style = {} } = this.resolve([ previousReactNativeStyle, reactNativeStyle ]);
// Because this is used in stateful operations we need to remove any
// existing inline styles that would override the classNames.
classList.forEach((className) => {
const { prop } = this.styleManager.getDeclaration(className);
style[prop] = null;
});
classList.push(preservedClassNames);
const className = classListToString(classList);
return { className, style };
}
/**
* Resolves a React Native style object
*/
_resolveStyle(reactNativeStyle) {
const domStyle = createReactDOMStyle(flattenStyle(reactNativeStyle));
const props = Object.keys(domStyle).reduce((props, styleProp) => {
const value = domStyle[styleProp];
if (value != null) {
const className = this.styleManager.getClassName(styleProp, value);
if (className) {
props.classList.push(className);
} else {
// 4x slower render
props.style[styleProp] = value;
}
}
return props;
}, { classList: [], style: {} });
const style = prefixInlineStyles(props.style);
props.className = classListToString(props.classList);
props.style = style;
return props;
}
/**
* Caching layer over 'resolveStyle'
*/
_resolveStyleIfNeeded(key, style) {
if (key) {
if (!this.cache[key]) {
// slow: convert style object to props and cache
this.cache[key] = this._resolveStyle(style);
}
return this.cache[key];
}
return this._resolveStyle(style);
}
}
module.exports = StyleRegistry;
@@ -0,0 +1,28 @@
/* eslint-env jasmine, jest */
import StyleManager from '../StyleManager';
let styleManager;
describe('apis/StyleSheet/StyleManager', () => {
beforeEach(() => {
styleManager = new StyleManager();
});
test('getClassName', () => {
expect(styleManager.getClassName('pointerEvents', 'box-only')).toMatchSnapshot();
const className = styleManager.setDeclaration('width', '100px');
expect(styleManager.getClassName('width', '100px')).toEqual(className);
});
test('getDeclaration', () => {
const className = styleManager.setDeclaration('width', '100px');
expect(styleManager.getDeclaration(className)).toEqual({ prop: 'width', value: '100px' });
});
test('getStyleSheetHtml', () => {
expect(styleManager.getStyleSheetHtml()).toMatchSnapshot();
styleManager.setDeclaration('width', '100px');
expect(styleManager.getStyleSheetHtml()).toMatchSnapshot();
});
});
@@ -0,0 +1,68 @@
/* eslint-env jasmine, jest */
import StyleRegistry from '../StyleRegistry';
let styleRegistry;
describe('apis/StyleSheet/StyleRegistry', () => {
beforeEach(() => {
styleRegistry = new StyleRegistry();
});
test('register', () => {
const style = { opacity: 0 };
const id = styleRegistry.register(style);
expect(typeof id === 'number').toBe(true);
});
describe('resolve', () => {
const styleA = { borderWidth: 0, borderColor: 'red', width: 100 };
const styleB = { position: 'absolute', left: 50, pointerEvents: 'box-only' };
const styleC = { width: 200 };
const testResolve = (a, b, c) => {
// no common properties, different resolving order, same result
const resolve1 = styleRegistry.resolve([ a, b ]);
expect(resolve1).toMatchSnapshot();
const resolve2 = styleRegistry.resolve([ b, a ]);
expect(resolve1).toEqual(resolve2);
// common properties, different resolving order, different result
const resolve3 = styleRegistry.resolve([ a, b, c ]);
expect(resolve3).toMatchSnapshot();
const resolve4 = styleRegistry.resolve([ c, a, b ]);
expect(resolve4).toMatchSnapshot();
expect(resolve3).not.toEqual(resolve4);
};
test('with register, resolves to className', () => {
const a = styleRegistry.register(styleA);
const b = styleRegistry.register(styleB);
const c = styleRegistry.register(styleC);
testResolve(a, b, c);
});
test('with register, resolves to mixed', () => {
const a = styleA;
const b = styleRegistry.register(styleB);
const c = styleRegistry.register(styleC);
testResolve(a, b, c);
});
test('without register, resolves to inline styles', () => {
testResolve(styleA, styleB, styleC);
});
});
test('resolveStateful', () => {
// generate a classList to act as pre-existing DOM state
const mockStyle = styleRegistry.register({ borderWidth: 0, borderColor: 'red', width: 100 });
const { classList: domClassList } = styleRegistry.resolve(mockStyle);
domClassList.unshift('external-className');
expect(domClassList).toMatchSnapshot();
// test result
const result = styleRegistry.resolveStateful({ borderWidth: 1, opacity: 1 }, domClassList);
expect(result).toMatchSnapshot();
});
});
+1 -16
View File
@@ -1,7 +1,6 @@
/* eslint-env jasmine, jest */
import StyleSheet from '..';
import StyleRegistry from '../registry';
const isPlainObject = (x) => {
const toString = Object.prototype.toString;
@@ -15,10 +14,6 @@ const isPlainObject = (x) => {
};
describe('apis/StyleSheet', () => {
beforeEach(() => {
StyleRegistry.reset();
});
test('absoluteFill', () => {
expect(Number.isInteger(StyleSheet.absoluteFill) === true).toBeTruthy();
});
@@ -29,7 +24,7 @@ describe('apis/StyleSheet', () => {
describe('create', () => {
test('replaces styles with numbers', () => {
const style = StyleSheet.create({ root: { opacity: 1 } });
const style = StyleSheet.create({ root: { position: 'absolute' } });
expect(Number.isInteger(style.root) === true).toBeTruthy();
});
});
@@ -43,16 +38,6 @@ describe('apis/StyleSheet', () => {
});
test('renderToString', () => {
StyleSheet.create({
a: {
borderWidth: 0,
borderColor: 'red'
},
b: {
position: 'absolute',
left: 50
}
});
expect(StyleSheet.renderToString()).toMatchSnapshot();
});
});
-22
View File
@@ -1,22 +0,0 @@
/* eslint-env jasmine, jest */
import injector from '../injector';
describe('apis/StyleSheet/injector', () => {
beforeEach(() => {
document.head.insertAdjacentHTML('afterbegin', `
<style id="react-native-stylesheet">
.rn-alignItems\\:stretch{align-items:stretch;}
.rn-position\\:top{position:top;}
</style>
`);
});
test('hydrates from SSR', () => {
const classList = injector.getClassNames();
expect(classList).toEqual({
'rn-alignItems\\:stretch': true,
'rn-position\\:top': true
});
});
});
@@ -1,54 +0,0 @@
/* eslint-env jasmine, jest */
import StyleRegistry from '../registry';
describe('apis/StyleSheet/registry', () => {
beforeEach(() => {
StyleRegistry.reset();
});
test('register', () => {
const style = { opacity: 0 };
const id = StyleRegistry.register(style);
expect(typeof id === 'number').toBe(true);
});
describe('resolve', () => {
const styleA = { borderWidth: 0, borderColor: 'red', width: 100 };
const styleB = { position: 'absolute', left: 50, pointerEvents: 'box-only' };
const styleC = { width: 200 };
const testResolve = (a, b, c) => {
// no common properties, different resolving order, same result
const resolve1 = StyleRegistry.resolve([ a, b ]);
expect(resolve1).toMatchSnapshot();
const resolve2 = StyleRegistry.resolve([ b, a ]);
expect(resolve1).toEqual(resolve2);
// common properties, different resolving order, different result
const resolve3 = StyleRegistry.resolve([ a, b, c ]);
expect(resolve3).toMatchSnapshot();
const resolve4 = StyleRegistry.resolve([ c, a, b ]);
expect(resolve4).toMatchSnapshot();
expect(resolve3).not.toEqual(resolve4);
};
test('with register, resolves to className', () => {
const a = StyleRegistry.register(styleA);
const b = StyleRegistry.register(styleB);
const c = StyleRegistry.register(styleC);
testResolve(a, b, c);
});
test('with register, resolves to mixed', () => {
const a = styleA;
const b = StyleRegistry.register(styleB);
const c = StyleRegistry.register(styleC);
testResolve(a, b, c);
});
test('without register, resolves to inline styles', () => {
testResolve(styleA, styleB, styleC);
});
});
});
+53
View File
@@ -0,0 +1,53 @@
/* eslint-disable */
/**
* JS Implementation of MurmurHash2
*
* @author <a href="mailto:gary.court@gmail.com">Gary Court</a>
* @see http://github.com/garycourt/murmurhash-js
* @author <a href="mailto:aappleby@gmail.com">Austin Appleby</a>
* @see http://sites.google.com/site/murmurhash/
*
* @param {string} str ASCII only
* @param {number} seed Positive integer only
* @return {number} 32-bit positive integer hash
*/
function murmurhash2_32_gc(str, seed) {
var
l = str.length,
h = seed ^ l,
i = 0,
k;
while (l >= 4) {
k =
((str.charCodeAt(i) & 0xff)) |
((str.charCodeAt(++i) & 0xff) << 8) |
((str.charCodeAt(++i) & 0xff) << 16) |
((str.charCodeAt(++i) & 0xff) << 24);
k = (((k & 0xffff) * 0x5bd1e995) + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16));
k ^= k >>> 24;
k = (((k & 0xffff) * 0x5bd1e995) + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16));
h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)) ^ k;
l -= 4;
++i;
}
switch (l) {
case 3: h ^= (str.charCodeAt(i + 2) & 0xff) << 16;
case 2: h ^= (str.charCodeAt(i + 1) & 0xff) << 8;
case 1: h ^= (str.charCodeAt(i) & 0xff);
h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16));
}
h ^= h >>> 13;
h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16));
h ^= h >>> 15;
return h >>> 0;
}
module.exports = (str) => murmurhash2_32_gc(str, 1).toString(36);
+12 -3
View File
@@ -1,3 +1,12 @@
const RE_1 = /([A-Z])/g;
const RE_2 = /^ms-/;
module.exports = (s) => s.replace(RE_1, '-$1').toLowerCase().replace(RE_2, '-ms-');
const cache = {};
const MS_REGEXP = /^ms-/;
const UPPERCASE_REGEXP = /([A-Z])/g;
const hyphenateStyleProp = (prop) => {
if (!cache.hasOwnProperty(prop)) {
cache[prop] = prop.replace(UPPERCASE_REGEXP, '-$&').toLowerCase().replace(MS_REGEXP, '-ms-');
}
return cache[prop];
};
module.exports = hyphenateStyleProp;
+5 -6
View File
@@ -1,14 +1,11 @@
import flattenStyle from './flattenStyle';
import initialize from './initialize';
import injector from './injector';
import StyleRegistry from './registry';
initialize();
const absoluteFillObject = { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0 };
const absoluteFill = StyleRegistry.register(absoluteFillObject);
module.exports = {
absoluteFill: StyleRegistry.register(absoluteFillObject),
absoluteFill,
absoluteFillObject,
create(styles) {
const result = {};
@@ -22,5 +19,7 @@ module.exports = {
},
hairlineWidth: 1,
flatten: flattenStyle,
renderToString: injector.getStyleSheetHtml
renderToString() {
return StyleRegistry.getStyleSheetHtml();
}
};
-39
View File
@@ -1,39 +0,0 @@
import injector from './injector';
import StyleRegistry from './registry';
const initialize = () => {
injector.addRule(
'reset',
'/* React Native StyleSheet*/\n' +
'html{' +
'font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;' +
'-webkit-tap-highlight-color:rgba(0,0,0,0)' +
'}\n' +
'body{margin:0}\n' +
'button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}\n' +
'input::-webkit-inner-spin-button,input::-webkit-outer-spin-button,' +
'input::-webkit-search-cancel-button,input::-webkit-search-decoration,' +
'input::-webkit-search-results-button,input::-webkit-search-results-decoration{display:none}'
);
injector.addRule(
'keyframes',
'@keyframes rn-ActivityIndicator-animation{' +
'0%{-webkit-transform: rotate(0deg); transform: rotate(0deg);}' +
'100%{-webkit-transform: rotate(360deg); transform: rotate(360deg);}' +
'}\n' +
'@keyframes rn-ProgressBar-animation{' +
'0%{-webkit-transform: translateX(-100%); transform: translateX(-100%);}' +
'100%{-webkit-transform: translateX(400%); transform: translateX(400%);}' +
'}'
);
injector.addRule(
'pointer-events',
'.rn-pointerEvents\\:auto,.rn-pointerEvents\\:box-only,.rn-pointerEvents\\:box-none *{pointer-events:auto}' +
'.rn-pointerEvents\\:none,.rn-pointerEvents\\:box-only *,.rn-pointerEvents\\:box-none{pointer-events:none}'
);
const classNames = injector.getClassNames();
StyleRegistry.initialize(classNames);
};
export default initialize;
-106
View File
@@ -1,106 +0,0 @@
/**
* Based on
* https://github.com/lelandrichardson/react-primitives/blob/master/src/StyleSheet/injector.js
*/
import asap from 'asap';
const emptyObject = {};
const hasOwnProperty = Object.prototype.hasOwnProperty;
const CLASSNAME_REXEP = /\.rn-([^{;\s]+)/g;
const STYLE_ELEMENT_ID = 'react-native-stylesheet';
let registry = {};
let isDirty = false;
/**
* Registers a rule and requests an update to the style sheet
*/
const addRule = (key, rule) => {
if (!registry[key]) {
registry[key] = rule;
isDirty = true;
if (global.document) {
asap(frame);
}
}
};
/**
* Returns a string of the registered rules
*/
const getStyleText = () => {
/* eslint prefer-template:0 */
let result = '\n';
for (const key in registry) {
if (hasOwnProperty.call(registry, key)) {
result += registry[key] + '\n';
}
}
return result;
};
/**
* Returns an HTML string for server rendering
*/
const getStyleSheetHtml = () => `<style id="${STYLE_ELEMENT_ID}">${getStyleText()}</style>`;
const reset = () => { registry = {}; };
/**
* Finds or injects the style sheet when in a browser environment
*/
let styleNode = null;
const getStyleNode = () => {
if (global.document) {
if (!styleNode) {
// look for existing style sheet (could also be server-rendered)
styleNode = document.getElementById(STYLE_ELEMENT_ID);
if (!styleNode) {
// if there is no existing stylesheet, inject it style sheet
document.head.insertAdjacentHTML('afterbegin', getStyleSheetHtml());
styleNode = document.getElementById(STYLE_ELEMENT_ID);
}
}
return styleNode;
}
};
/**
* Determines which classes are available in the existing document. Doesn't
* rely on the registry so it can be used to read class names from a SSR style
* sheet.
*/
const getClassNames = () => {
const styleNode = getStyleNode();
if (styleNode) {
const text = styleNode.textContent;
const matches = text.match(CLASSNAME_REXEP);
if (matches) {
return matches.map((name) => name.slice(1)).reduce((classMap, className) => {
classMap[className] = true;
return classMap;
}, {});
}
}
return emptyObject;
};
const frame = () => {
if (!isDirty || !global.document) { return; }
isDirty = false;
const styleNode = getStyleNode();
if (styleNode) {
const css = getStyleText();
styleNode.textContent = css;
}
};
module.exports = {
addRule,
getClassNames,
getStyleSheetHtml,
reset
};
+3 -202
View File
@@ -1,202 +1,3 @@
/**
* WARNING: changes to this file in particular can cause significant changes to
* the results of render performance benchmarks.
*/
import createReactDOMStyle from './createReactDOMStyle';
import flattenArray from '../../modules/flattenArray';
import flattenStyle from './flattenStyle';
import generateCss from './generateCss';
import I18nManager from '../I18nManager';
import injector from './injector';
import mapKeyValue from '../../modules/mapKeyValue';
import prefixInlineStyles from './prefixInlineStyles';
import ReactNativePropRegistry from '../../modules/ReactNativePropRegistry';
const SPACE_REGEXP = /\s/g;
const ESCAPE_SELECTOR_CHARS_REGEXP = /[(),":?.%\\$#*]/g;
const createCacheKey = (id) => {
const prefix = I18nManager.isRTL ? 'rtl' : 'ltr';
return `${prefix}-${id}`;
};
/**
* Creates an HTML class name for use on elements
*/
const createClassName = (prop, value) => {
const val = `${value}`.replace(SPACE_REGEXP, '-');
return `rn-${prop}:${val}`;
};
/**
* Formatting improves debugging in devtools and snapshot
*/
const mapDeclarationsToClassName = (style, fn) => {
const result = mapKeyValue(style, fn).join('\n').trim();
return `\n${result}`;
};
/**
* Inject a CSS rule for a given declaration and record the availability of the
* resulting class name.
*/
let injectedClassNames = {};
const injectClassNameIfNeeded = (prop, value) => {
const className = createClassName(prop, value);
if (!injectedClassNames[className]) {
// create a valid CSS selector for a given HTML class name
const selector = className.replace(ESCAPE_SELECTOR_CHARS_REGEXP, '\\$&');
const body = generateCss({ [prop]: value });
const css = `.${selector}{${body}}`;
// this adds the rule to the buffer to be injected into the document
injector.addRule(className, css);
injectedClassNames[className] = true;
}
return className;
};
/**
* Converts a React Native style object to HTML class names
*/
let resolvedPropsCache = {};
const registerStyle = (id, flatStyle) => {
const style = createReactDOMStyle(flatStyle);
const className = mapDeclarationsToClassName(style, (prop, value) => {
if (value != null) {
return injectClassNameIfNeeded(prop, value);
}
});
const key = createCacheKey(id);
resolvedPropsCache[key] = { className };
return id;
};
/**
* Resolves a React Native style object to DOM props
*/
const resolveProps = (reactNativeStyle) => {
const flatStyle = flattenStyle(reactNativeStyle);
const domStyle = createReactDOMStyle(flatStyle);
const style = {};
const className = mapDeclarationsToClassName(domStyle, (prop, value) => {
if (value != null) {
const singleClassName = createClassName(prop, value);
if (injectedClassNames[singleClassName]) {
return singleClassName;
} else {
// 4x slower render
style[prop] = value;
}
}
});
const props = {
className,
style: prefixInlineStyles(style)
};
/*
if (process.env.__REACT_NATIVE_DEBUG_ENABLED__) {
console.groupCollapsed('[StyleSheet] resolving uncached styles');
console.log(
'Slow operation. Resolving style objects (uncached result). ' +
'Occurs on first render and when using styles not registered with "StyleSheet.create"'
);
console.log('source => \n', reactNativeStyle);
console.log('flatten => \n', flatStyle);
console.log('resolve => \n', props);
console.groupEnd();
}
*/
return props;
};
/**
* Caching layer over 'resolveProps'
*/
const resolvePropsIfNeeded = (key, style) => {
if (key) {
if (!resolvedPropsCache[key]) {
// slow: convert style object to props and cache
resolvedPropsCache[key] = resolveProps(style);
}
return resolvedPropsCache[key];
}
return resolveProps(style);
};
/**
* Web style registry
*/
const StyleRegistry = {
initialize(classNames) {
injectedClassNames = classNames;
/*
if (process.env.__REACT_NATIVE_DEBUG_ENABLED__) {
if (global.__REACT_NATIVE_DEBUG_ENABLED__styleRegistryTimer) {
clearInterval(global.__REACT_NATIVE_DEBUG_ENABLED__styleRegistryTimer);
}
global.__REACT_NATIVE_DEBUG_ENABLED__styleRegistryTimer = setInterval(() => {
const entryCount = Object.keys(resolvedPropsCache).length;
console.groupCollapsed('[StyleSheet] resolved props cache snapshot:', entryCount, 'entries');
console.log(resolvedPropsCache);
console.groupEnd();
}, 30000);
}
*/
},
reset() {
injectedClassNames = {};
resolvedPropsCache = {};
injector.reset();
},
register(style) {
const id = ReactNativePropRegistry.register(style);
return registerStyle(id, style);
},
resolve(reactNativeStyle) {
if (!reactNativeStyle) {
return undefined;
}
// fast and cachable
if (typeof reactNativeStyle === 'number') {
const key = createCacheKey(reactNativeStyle);
return resolvePropsIfNeeded(key, reactNativeStyle);
}
// convert a RN style object to DOM props
if (!Array.isArray(reactNativeStyle)) {
return resolveProps(reactNativeStyle);
}
// flatten the array
// [ 1, [ 2, 3 ], { prop: value }, 4, 5 ] => [ 1, 2, 3, { prop: value }, 4, 5 ];
const flatArray = flattenArray(reactNativeStyle);
let isArrayOfNumbers = true;
for (let i = 0; i < flatArray.length; i++) {
if (typeof flatArray[i] !== 'number') {
isArrayOfNumbers = false;
break;
}
}
// cache resolved props when all styles are registered
const key = isArrayOfNumbers ? createCacheKey(flatArray.join('-')) : null;
return resolvePropsIfNeeded(key, flatArray);
}
};
module.exports = StyleRegistry;
import StyleRegistry from './StyleRegistry';
const registry = new StyleRegistry();
module.exports = registry;
+21
View File
@@ -0,0 +1,21 @@
module.exports =
// css reset
'html{' +
'font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;' +
'-webkit-tap-highlight-color:rgba(0,0,0,0);' +
'}\n' +
'body{margin:0;}\n' +
'button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}\n' +
'input::-webkit-inner-spin-button,input::-webkit-outer-spin-button,' +
'input::-webkit-search-cancel-button,input::-webkit-search-decoration,' +
'input::-webkit-search-results-button,input::-webkit-search-results-decoration{display:none;}\n' +
'@keyframes rn-ActivityIndicator-animation{' +
'0%{-webkit-transform:rotate(0deg);transform:rotate(0deg);}' +
'100%{-webkit-transform:rotate(360deg);transform:rotate(360deg);}' +
'}\n' +
'@keyframes rn-ProgressBar-animation{' +
'0%{-webkit-transform:translateX(-100%);transform:translateX(-100%)};' +
'100%{-webkit-transform:translateX(400%);transform:translateX(400%)};' +
'}';
+3 -47
View File
@@ -10,19 +10,6 @@ import findNodeHandle from '../findNodeHandle';
import StyleRegistry from '../../apis/StyleSheet/registry';
import UIManager from '../../apis/UIManager';
const emptyObject = {};
const REGEX_CLASSNAME_SPLIT = /\s+/;
const REGEX_STYLE_PROP = /rn-(.*):.*/;
const classNameFilter = (className) => { return className !== ''; };
const classNameToList = (className = '') => className.split(REGEX_CLASSNAME_SPLIT).filter(classNameFilter);
const getStyleProp = (className) => {
const match = className.match(REGEX_STYLE_PROP);
if (match) {
return match[1];
}
};
const NativeMethodsMixin = {
/**
* Removes focus from an input or view. This is the opposite of `focus()`.
@@ -81,41 +68,10 @@ const NativeMethodsMixin = {
setNativeProps(nativeProps: Object) {
// DOM state
const node = findNodeHandle(this);
const domClassList = [ ...node.classList ];
const classList = [ ...node.classList ];
// Resolved state
const resolvedProps = StyleRegistry.resolve(nativeProps.style) || emptyObject;
const resolvedClassList = classNameToList(resolvedProps.className);
// Merged state
const classList = [];
const style = { ...resolvedProps.style };
// The node has class names that we need to override.
// Only pass on a class name when the style is unchanged.
domClassList.forEach((c) => {
const prop = getStyleProp(c);
const className = resolvedProps.className;
if (!className || className.indexOf(prop) === -1) {
classList.push(c);
}
});
// The node has styles that we need to override.
// Remove any inline style that may collide with a new class name.
resolvedClassList.forEach((c) => {
const prop = getStyleProp(c);
classList.push(c);
style[prop] = null;
});
const className = `\n${classList.sort().join('\n')}`;
const props = {
...nativeProps,
className,
style
};
const { className, style } = StyleRegistry.resolveStateful(nativeProps.style, classList);
const props = { ...nativeProps, className, style };
UIManager.updateView(node, props, this);
}
+7 -4
View File
@@ -45,10 +45,13 @@ const modality = () => {
/**
* Disable the focus ring by default
*/
const id = 'modality__';
const style = `<style id="${id}">:focus { outline: none; }</style>`;
document.head.insertAdjacentHTML('afterbegin', style);
const styleElement = document.getElementById(id);
const id = 'react-native-modality';
let styleElement = document.getElementById(id);
if (!styleElement) {
const style = `<style id="${id}">:focus { outline: none; }</style>`;
document.head.insertAdjacentHTML('afterbegin', style);
styleElement = document.getElementById(id);
}
const disableFocus = () => {
if (styleElement) {