From 0361845537f38501e5be900531627fcbd5a74b0f Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Mon, 20 Feb 2017 13:16:27 -0800 Subject: [PATCH] [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 --- src/apis/StyleSheet/StyleManager.js | 127 +++++++++++ src/apis/StyleSheet/StyleRegistry.js | 158 ++++++++++++++ .../StyleSheet/__tests__/StyleManager-test.js | 28 +++ .../__tests__/StyleRegistry-test.js | 68 ++++++ src/apis/StyleSheet/__tests__/index-test.js | 17 +- src/apis/StyleSheet/__tests__/injector.js | 22 -- .../StyleSheet/__tests__/registry-test.js | 54 ----- src/apis/StyleSheet/hash.js | 53 +++++ src/apis/StyleSheet/hyphenate.js | 15 +- src/apis/StyleSheet/index.js | 11 +- src/apis/StyleSheet/initialize.js | 39 ---- src/apis/StyleSheet/injector.js | 106 --------- src/apis/StyleSheet/registry.js | 205 +----------------- src/apis/StyleSheet/staticCss.js | 21 ++ src/modules/NativeMethodsMixin/index.js | 50 +---- src/modules/modality/index.js | 11 +- 16 files changed, 486 insertions(+), 499 deletions(-) create mode 100644 src/apis/StyleSheet/StyleManager.js create mode 100644 src/apis/StyleSheet/StyleRegistry.js create mode 100644 src/apis/StyleSheet/__tests__/StyleManager-test.js create mode 100644 src/apis/StyleSheet/__tests__/StyleRegistry-test.js delete mode 100644 src/apis/StyleSheet/__tests__/injector.js delete mode 100644 src/apis/StyleSheet/__tests__/registry-test.js create mode 100644 src/apis/StyleSheet/hash.js delete mode 100644 src/apis/StyleSheet/initialize.js delete mode 100644 src/apis/StyleSheet/injector.js create mode 100644 src/apis/StyleSheet/staticCss.js diff --git a/src/apis/StyleSheet/StyleManager.js b/src/apis/StyleSheet/StyleManager.js new file mode 100644 index 00000000..ed0a8ebe --- /dev/null +++ b/src/apis/StyleSheet/StyleManager.js @@ -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 = ``; + const mainSheet = ``; + 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; diff --git a/src/apis/StyleSheet/StyleRegistry.js b/src/apis/StyleSheet/StyleRegistry.js new file mode 100644 index 00000000..57507cb9 --- /dev/null +++ b/src/apis/StyleSheet/StyleRegistry.js @@ -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; diff --git a/src/apis/StyleSheet/__tests__/StyleManager-test.js b/src/apis/StyleSheet/__tests__/StyleManager-test.js new file mode 100644 index 00000000..fc07d905 --- /dev/null +++ b/src/apis/StyleSheet/__tests__/StyleManager-test.js @@ -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(); + }); +}); diff --git a/src/apis/StyleSheet/__tests__/StyleRegistry-test.js b/src/apis/StyleSheet/__tests__/StyleRegistry-test.js new file mode 100644 index 00000000..41bf0543 --- /dev/null +++ b/src/apis/StyleSheet/__tests__/StyleRegistry-test.js @@ -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(); + }); +}); diff --git a/src/apis/StyleSheet/__tests__/index-test.js b/src/apis/StyleSheet/__tests__/index-test.js index e8166c39..52ca1f89 100644 --- a/src/apis/StyleSheet/__tests__/index-test.js +++ b/src/apis/StyleSheet/__tests__/index-test.js @@ -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(); }); }); diff --git a/src/apis/StyleSheet/__tests__/injector.js b/src/apis/StyleSheet/__tests__/injector.js deleted file mode 100644 index 0e50932f..00000000 --- a/src/apis/StyleSheet/__tests__/injector.js +++ /dev/null @@ -1,22 +0,0 @@ -/* eslint-env jasmine, jest */ - -import injector from '../injector'; - -describe('apis/StyleSheet/injector', () => { - beforeEach(() => { - document.head.insertAdjacentHTML('afterbegin', ` - - `); - }); - - test('hydrates from SSR', () => { - const classList = injector.getClassNames(); - expect(classList).toEqual({ - 'rn-alignItems\\:stretch': true, - 'rn-position\\:top': true - }); - }); -}); diff --git a/src/apis/StyleSheet/__tests__/registry-test.js b/src/apis/StyleSheet/__tests__/registry-test.js deleted file mode 100644 index f63edc61..00000000 --- a/src/apis/StyleSheet/__tests__/registry-test.js +++ /dev/null @@ -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); - }); - }); -}); diff --git a/src/apis/StyleSheet/hash.js b/src/apis/StyleSheet/hash.js new file mode 100644 index 00000000..b1f8167d --- /dev/null +++ b/src/apis/StyleSheet/hash.js @@ -0,0 +1,53 @@ +/* eslint-disable */ +/** + * JS Implementation of MurmurHash2 + * + * @author Gary Court + * @see http://github.com/garycourt/murmurhash-js + * @author Austin Appleby + * @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); diff --git a/src/apis/StyleSheet/hyphenate.js b/src/apis/StyleSheet/hyphenate.js index 71fca3d4..a42222d2 100644 --- a/src/apis/StyleSheet/hyphenate.js +++ b/src/apis/StyleSheet/hyphenate.js @@ -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; diff --git a/src/apis/StyleSheet/index.js b/src/apis/StyleSheet/index.js index 3e90789b..842e482b 100644 --- a/src/apis/StyleSheet/index.js +++ b/src/apis/StyleSheet/index.js @@ -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(); + } }; diff --git a/src/apis/StyleSheet/initialize.js b/src/apis/StyleSheet/initialize.js deleted file mode 100644 index 5315ac32..00000000 --- a/src/apis/StyleSheet/initialize.js +++ /dev/null @@ -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; diff --git a/src/apis/StyleSheet/injector.js b/src/apis/StyleSheet/injector.js deleted file mode 100644 index 1eccd631..00000000 --- a/src/apis/StyleSheet/injector.js +++ /dev/null @@ -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 = () => ``; - -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 -}; diff --git a/src/apis/StyleSheet/registry.js b/src/apis/StyleSheet/registry.js index 193a6930..d067b76f 100644 --- a/src/apis/StyleSheet/registry.js +++ b/src/apis/StyleSheet/registry.js @@ -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; diff --git a/src/apis/StyleSheet/staticCss.js b/src/apis/StyleSheet/staticCss.js new file mode 100644 index 00000000..b0f9e8b5 --- /dev/null +++ b/src/apis/StyleSheet/staticCss.js @@ -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%)};' + + '}'; diff --git a/src/modules/NativeMethodsMixin/index.js b/src/modules/NativeMethodsMixin/index.js index 576fe7e6..4a6abda2 100644 --- a/src/modules/NativeMethodsMixin/index.js +++ b/src/modules/NativeMethodsMixin/index.js @@ -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); } diff --git a/src/modules/modality/index.js b/src/modules/modality/index.js index 58bf4601..bc906b55 100644 --- a/src/modules/modality/index.js +++ b/src/modules/modality/index.js @@ -45,10 +45,13 @@ const modality = () => { /** * Disable the focus ring by default */ - const id = 'modality__'; - const 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 = ``; + document.head.insertAdjacentHTML('afterbegin', style); + styleElement = document.getElementById(id); + } const disableFocus = () => { if (styleElement) {