diff --git a/src/renderers/dom/ReactDOM.js b/src/renderers/dom/ReactDOM.js index c68819b43ea..b0b86ea9c25 100644 --- a/src/renderers/dom/ReactDOM.js +++ b/src/renderers/dom/ReactDOM.js @@ -67,6 +67,14 @@ if ( if (__DEV__) { var ExecutionEnvironment = require('ExecutionEnvironment'); + if ( + ExecutionEnvironment.canUseDOM && + typeof window.__showWarning === 'undefined' + ) { + // install the `__showWarning` global hook for yellow box + window.__showWarning = require('reactShowWarningDOM'); + } + if (ExecutionEnvironment.canUseDOM && window.top === window.self) { // First check if devtools is not installed diff --git a/src/renderers/dom/client/showWarningDOM/components/ReactDOMYellowBoxDialogBody.js b/src/renderers/dom/client/showWarningDOM/components/ReactDOMYellowBoxDialogBody.js new file mode 100644 index 00000000000..5adc766bff3 --- /dev/null +++ b/src/renderers/dom/client/showWarningDOM/components/ReactDOMYellowBoxDialogBody.js @@ -0,0 +1,44 @@ +/** + * Copyright 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactDOMYellowBoxDialogBody + * @flow + */ +'use strict'; + +const React = require('React'); + +const ReactDOMYellowBoxMessage = require('ReactDOMYellowBoxMessage'); + +import type {Format, Instance, Milliseconds, InstanceInfo} from 'reactShowWarningDOM'; + +const styles = { + root: { + height: '90%', + overflowY: 'auto', + }, +}; + +const ReactDOMYellowBoxDialogBody = ({data, onSnoozeByType, onSnoozeByInstance}: { + data: Array, + onSnoozeByType: (format: Format) => (snoozeDuration: Milliseconds) => void, + onSnoozeByInstance: (instance: Instance) => (snoozeDuration: Milliseconds) => void, +}) => ( +
+ {data.map(({instance, format}) => + + )} +
+); + +module.exports = ReactDOMYellowBoxDialogBody; diff --git a/src/renderers/dom/client/showWarningDOM/components/ReactDOMYellowBoxDialogHeader.js b/src/renderers/dom/client/showWarningDOM/components/ReactDOMYellowBoxDialogHeader.js new file mode 100644 index 00000000000..faaa53b3aa9 --- /dev/null +++ b/src/renderers/dom/client/showWarningDOM/components/ReactDOMYellowBoxDialogHeader.js @@ -0,0 +1,54 @@ +/** + * Copyright 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactDOMYellowBoxDialogHeader + * @flow + */ +'use strict'; + +const React = require('React'); + +const styles = { + root: { + paddingBottom: '15px', + borderBottom: '1px solid rgba(0, 0, 0, 0.10)', + }, + closeButton: { + color: 'inherit', + textDecoration: 'none', + }, + closeButtonContainer: { + float: 'right', + }, +}; + +const ReactDOMYellowBoxDialogHeader = ({onIgnoreAll}: { + onIgnoreAll: () => void, +}) => { + const onClick = (evt) => { + evt.preventDefault(); + onIgnoreAll(); + }; + + return ( +
+
+ + ✕ + +
+ React Warnings +
+ ); +}; + +module.exports = ReactDOMYellowBoxDialogHeader; diff --git a/src/renderers/dom/client/showWarningDOM/components/ReactDOMYellowBoxMessage.js b/src/renderers/dom/client/showWarningDOM/components/ReactDOMYellowBoxMessage.js new file mode 100644 index 00000000000..ed6e691c415 --- /dev/null +++ b/src/renderers/dom/client/showWarningDOM/components/ReactDOMYellowBoxMessage.js @@ -0,0 +1,94 @@ +/** + * Copyright 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactDOMYellowBoxMessage + * @flow + */ +'use strict'; + +const React = require('React'); + +import type {Instance, Milliseconds} from 'reactShowWarningDOM'; + +const MS_FOR_10_MINS: Milliseconds = 10 * 60 * 1000; +const MS_FOR_24_HOURS: Milliseconds = 24 * 3600 * 1000; + +const styles = { + root: { + fontSize: '13px', + borderBottom: '1px solid rgba(0, 0, 0, 0.05)', + padding: '20px 0px', + }, + snoozeButtonContainer: { + float: 'right', + }, + snoozeSelect: { + width: '70px', + }, + messageContainer: { + fontFamily: 'Menlo, Consolas, monospace', + whiteSpace: 'pre-wrap', + }, +}; + +const ReactDOMYellowBoxMessage = ({onSnoozeByType, onSnoozeByInstance, instance}: { + onSnoozeByType: (snoozeDuration: Milliseconds) => void, + onSnoozeByInstance: (snoozeDuration: Milliseconds) => void, + instance: Instance, +}) => { + const onSelectChange = (evt: SyntheticEvent): void => { + evt.preventDefault(); + + if (!(evt.target instanceof HTMLSelectElement)) { // make flow happy + return; + } + + switch (evt.target.value) { + case 'type-10-mins': + onSnoozeByType(MS_FOR_10_MINS); + break; + case 'type-24-hrs': + onSnoozeByType(MS_FOR_24_HOURS); + break; + case 'instance-10-mins': + onSnoozeByInstance(MS_FOR_10_MINS); + break; + case 'instance-24-hrs': + onSnoozeByInstance(MS_FOR_24_HOURS); + break; + default: + break; + } + }; + + return ( +
+
+ +
+
+ {instance} +
+
+ ); +}; + +module.exports = ReactDOMYellowBoxMessage; diff --git a/src/renderers/dom/client/showWarningDOM/components/ReactDOMYellowBoxRoot.js b/src/renderers/dom/client/showWarningDOM/components/ReactDOMYellowBoxRoot.js new file mode 100644 index 00000000000..137e26800ae --- /dev/null +++ b/src/renderers/dom/client/showWarningDOM/components/ReactDOMYellowBoxRoot.js @@ -0,0 +1,71 @@ +/** + * Copyright 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactDOMYellowBoxRoot + * @flow + */ +'use strict'; + +const React = require('React'); + +const ReactDOMYellowBoxDialogHeader = require('ReactDOMYellowBoxDialogHeader'); +const ReactDOMYellowBoxDialogBody = require('ReactDOMYellowBoxDialogBody'); + +import type {Format, Instance, Milliseconds, InstanceInfo} from 'reactShowWarningDOM'; + +const styles = { + root: { + position: 'fixed', + top: 0, + right: 0, + bottom: 0, + left: 0, + pointerEvents: 'none', + zIndex: 2147483647, + }, + dialogRoot: { + position: 'absolute', + height: '45%', + width: '75%', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + MozTransform: 'translate(-50%, -50%)', + msTransform: 'translate(-50%, -50%)', + WebkitTransform: 'translate(-50%, -50%)', + padding: '15px 30px', + pointerEvents: 'auto', + overflowY: 'hidden', + backgroundColor: 'rgb(254, 243, 161)', + border: '1px solid rgba(0, 0, 0, 0.10)', + borderRadius: '2px', + boxShadow: '0px 0px 5px rgba(0, 0, 0, 0.10)', + fontFamily: 'Helvetica, Arial, sans-serif', + fontSize: '15px', + }, +}; + +const ReactDOMYellowBoxRoot = ({data, onIgnoreAll, onSnoozeByType, onSnoozeByInstance}: { + data: Array, + onIgnoreAll: () => void, + onSnoozeByType: (format: Format) => (snoozeDuration: Milliseconds) => void, + onSnoozeByInstance: (instance: Instance) => (snoozeDuration: Milliseconds) => void, +}) => ( +
+
+ + +
+
+); + +module.exports = ReactDOMYellowBoxRoot; diff --git a/src/renderers/dom/client/showWarningDOM/reactShowWarningDOM.js b/src/renderers/dom/client/showWarningDOM/reactShowWarningDOM.js new file mode 100644 index 00000000000..8759b815014 --- /dev/null +++ b/src/renderers/dom/client/showWarningDOM/reactShowWarningDOM.js @@ -0,0 +1,232 @@ +/** + * Copyright 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule reactShowWarningDOM + * @flow + */ +'use strict'; + +const React = require('React'); +const ReactMount = require('ReactMount'); + +const ReactDOMYellowBoxRoot = require('ReactDOMYellowBoxRoot'); + +export type Format = string; +export type Instance = string; +export type Milliseconds = number; +export type InstanceInfo = {format: Format, instance: Instance}; + +type WarningStore = { + instanceList: Array, + snoozeCache: { + formatSnoozes: { + [format: Format]: Milliseconds, + }, + instanceSnoozes: { + [instance: Instance]: Milliseconds, + }, + }, +}; + +const containerElemID = 'react.yellow_Box'; +const localSnoozeDataKey = '_$reactYellowBoxSnoozeData'; + +const detectLocalStorage = (): boolean => { + var testMsg = '_$detectLocalStorage'; + try { + localStorage.setItem(testMsg, testMsg); + localStorage.removeItem(testMsg); + return true; + } catch (e) { + return false; + } +}; + +const canUseLocalStorage = detectLocalStorage(); + +/* + * we only save snooze data to localStorage. + * here we read it from localStorage and remove expired timestamps + */ +const getAndUpdateWarningStore = (): WarningStore => { + let snoozeCache = { + formatSnoozes: {}, + instanceSnoozes: {}, + }; + + if (canUseLocalStorage) { + const rawSnoozeData = localStorage.getItem(localSnoozeDataKey); + if (rawSnoozeData) { + const localSnoozeData = JSON.parse(rawSnoozeData); + const {formatSnoozes, instanceSnoozes} = localSnoozeData; + const timeNow = Date.now(); + + for (let format in formatSnoozes) { + const until = formatSnoozes[format]; + if (timeNow < until) { + // not expired; add it to the in-memory cache + snoozeCache.formatSnoozes[format] = until; + } + } + + for (let instance in instanceSnoozes) { + const until = instanceSnoozes[instance]; + if (timeNow < until) { + // not expired; add it to the in-memory cache + snoozeCache.instanceSnoozes[instance] = until; + } + } + + // remove expired records + localStorage.setItem( + localSnoozeDataKey, + JSON.stringify(snoozeCache) + ); + } + } + + return { + instanceList: [], + snoozeCache, + }; +}; + +const warningStore = getAndUpdateWarningStore(); + +/* + * updates the in-memory warningStore. doesn't touch the snooze part. + */ +const updateWarningStore = ( + instance: Instance, + format: Format, + args: Array, +): void => { + const {instanceList} = warningStore; + + for (let i = 0; i < instanceList.length; i++) { + const cursor = instanceList[i]; + if (cursor.instance === instance) { // already exists + return; + } + } + + instanceList.push({ + format, + instance, + }); +}; + +/* + * gets data for the Yellow Box component. + * validates timestamps. + */ +const getDataFromWarningStore = (): Array => { + const timeNow = Date.now(); + + const {instanceList, snoozeCache} = warningStore; + const {formatSnoozes, instanceSnoozes} = snoozeCache; + + return instanceList.filter(({format, instance}) => { + if ( + (instanceSnoozes[instance] && timeNow < instanceSnoozes[instance]) || + (formatSnoozes[format] && timeNow < formatSnoozes[format]) + ) { + // still snoozing! + return false; + } + + return true; + }); +}; + +const unmountComponentAndRemoveElem = (containerElem: HTMLElement): void => { + ReactMount.unmountComponentAtNode(containerElem); + containerElem.remove(); +}; + +const renderYellowBox = (): void => { + const data = getDataFromWarningStore(); + + let containerElem = document.getElementById(containerElemID); + + // unmounts Yellow Box when there's no warning to display + if (!data.length) { + if (containerElem) { + unmountComponentAndRemoveElem(containerElem); + } + return; + } + + if (!containerElem) { + containerElem = document.createElement('div'); + containerElem.setAttribute('id', containerElemID); + document.body.appendChild(containerElem); + } + + const unmountYellowBox = () => unmountComponentAndRemoveElem(containerElem); + + /* + * Warnings often happen during rendering and we must not + * trigger a new render call from an ongoing rendering. + */ + setTimeout(() => { + ReactMount.render( + , + containerElem + ); + }, 0); +}; + +/* + * updates the timestamp for a type of warning messages. + * writes updated snooze data to localStorage. + */ +const updateSnoozeForFormat = (format: Format) => (snoozeDuration: Milliseconds): void => { + warningStore.snoozeCache.formatSnoozes[format] = Date.now() + snoozeDuration; + if (canUseLocalStorage) { + localStorage.setItem( + localSnoozeDataKey, + JSON.stringify(warningStore.snoozeCache) + ); + } + renderYellowBox(); +}; + +/* + * updates the timestamp for a warning message instance. + * writes updated snooze data to localStorage. + */ +const updateSnoozeForInstance = (instance: Instance) => (snoozeDuration: Milliseconds): void => { + warningStore.snoozeCache.instanceSnoozes[instance] = Date.now() + snoozeDuration; + if (canUseLocalStorage) { + localStorage.setItem( + localSnoozeDataKey, + JSON.stringify(warningStore.snoozeCache) + ); + } + renderYellowBox(); +}; + +/* + * main entry for `warning` (fbjs) to call. + */ +const reactShowWarningDOM = ({message, format, args}: { + message: Instance, + format: Format, + args: Array, +}): void => { + updateWarningStore(message, format, args); + renderYellowBox(); +}; + +module.exports = reactShowWarningDOM;