mirror of
https://github.com/zoriya/react-native-web.git
synced 2026-08-15 18:45:06 +00:00
[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:
@@ -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;
|
||||
@@ -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,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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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%)};' +
|
||||
'}';
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user