From 67c6de4a938c13255fb47c63de2e8e98988c1a5e Mon Sep 17 00:00:00 2001 From: klimashkin Date: Thu, 15 Mar 2018 22:41:52 -0700 Subject: [PATCH] First release Signed-off-by: klimashkin --- .editorconfig | 11 + .eslintignore | 1 + .eslintrc.js | 608 +++++++++++++++++++++++++++++++++++++ .gitignore | 9 +- .npmrc | 1 + CHANGELOG.md | 4 + README.md | 4 +- package.json | 82 +++++ src/SizeWatcher.js | 245 +++++++++++++++ src/SizeWatcherProvider.js | 63 ++++ src/SizeWatcherTypes.js | 8 + src/index.js | 6 + webpack.config.js | 125 ++++++++ 13 files changed, 1159 insertions(+), 8 deletions(-) create mode 100644 .editorconfig create mode 100644 .eslintignore create mode 100644 .eslintrc.js create mode 100644 .npmrc create mode 100644 CHANGELOG.md create mode 100644 package.json create mode 100644 src/SizeWatcher.js create mode 100644 src/SizeWatcherProvider.js create mode 100644 src/SizeWatcherTypes.js create mode 100644 src/index.js create mode 100644 webpack.config.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4b7d6b4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +charset = utf-8 + +indent_style = space +indent_size = 2 + +end_of_line = lf +insert_final_newline = false +trim_trailing_whitespace = true diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..3693d63 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +dist/**/*.js \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..3cc1e81 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,608 @@ +const path = require('path'); + +module.exports = { + 'root': true, + 'parser': 'babel-eslint', + + 'env': { + 'amd': true, + 'es6': true, + 'jest': true, + 'node': true, + 'browser': true, + 'serviceworker': true, + }, + + 'parserOptions': { + 'ecmaVersion': 2017, + 'sourceType': 'module', + 'ecmaFeatures': { + 'jsx': true, + 'objectLiteralDuplicateProperties': false + }, + 'codeFrame': true, // Show the code frame in the reporter + }, + + 'plugins': [ + 'react', + 'babel', + 'import', + ], + + 'settings': { + 'react': { + 'pragma': 'React', // Pragma to use, default to "React" + 'version': '16.2' // React version, default to the latest React stable release, needed also for react/no-deprecated + }, + // Avoid the copious amount of fs.statSync/module parse calls required to correctly report errors + 'import/cache': 100, + // File extensions that will be parsed as modules and inspected for exports + 'import/extensions': ['.js', '.jsx'], + // Import resolvers + 'import/resolver': { + 'node': { + moduleDirectory: 'node_modules', + }, + }, + }, + + 'rules': { + // babel inserts `'use strict';` for us + 'strict': [2, 'never'], + + /** ES6 section http://eslint.org/docs/rules/#ecmascript-6 */ + // enforces no braces where they can be omitted + 'arrow-body-style': [2, 'as-needed', { 'requireReturnForObjectLiteral': false }], + // require parens in arrow function arguments + 'arrow-parens': [2, 'as-needed'], + // require space before/after arrow function's arrow + 'arrow-spacing': [2, { 'before': true, 'after': true }], + // require trailing commas in multiline object literals + 'comma-dangle': [2, { + 'arrays': 'always-multiline', + 'objects': 'always-multiline', + 'imports': 'always-multiline', + 'exports': 'always-multiline', + 'functions': 'ignore', + }], + // enforce the spacing around the * in generator functions + 'generator-star-spacing': [2, { + 'before': false, + 'after': true, + 'method': { 'before': true, 'after': true }, + }], + // disallow modifying variables of class declarations + 'no-class-assign': 2, + // disallow modifying variables that are declared using const + 'no-const-assign': 2, + // disallow duplicate class members + 'no-dupe-class-members': 2, + // disallow symbol constructor + 'no-new-symbol': 2, + // Require let or const instead of var + 'no-var': 2, + // disallow unnecessary computed property keys in object literals + 'no-useless-computed-key': 2, + // disallow unnecessary constructor + 'no-useless-constructor': 2, + // disallow renaming import, export, and destructured assignments to the same name + 'no-useless-rename': 2, + // require method and property shorthand syntax for object literals + 'object-shorthand': [2, 'always', { 'avoidQuotes': true }], + // suggest using arrow functions as callbacks + 'prefer-arrow-callback': [2, { 'allowNamedFunctions': true }], + // suggest using of const declaration for variables that are never modified after declared + // destructuring:all means if some variable within destructuring is modified later(let), + // even if others never(const), whole destructuring can be defined as let + 'prefer-const': [2, { 'destructuring': 'all', 'ignoreReadBeforeAssign': true }], + // disallow parseInt() in favor of binary, octal, and hexadecimal literals + 'prefer-numeric-literals': 2, + // Suggest using the spread operator instead of .apply() + 'prefer-spread': 2, + // use rest parameters instead of arguments + 'prefer-rest-params': 2, + // require symbol descriptions + 'symbol-description': 2, + // enforce usage of spacing in template strings + 'template-curly-spacing': 2, + // enforce spacing around the * in yield* expressions + 'yield-star-spacing': [2, 'after'], + + + /** Best Practices section http://eslint.org/docs/rules/#best-practices **/ + // specify curly brace conventions for all control statements + 'curly': [2, 'multi-line'], + // encourages use of dot notation whenever possible + 'dot-notation': 2, + // enforces consistent newlines before or after dots + 'dot-location': [2, 'property'], + // require the use of === and !== + 'eqeqeq': 2, + // disallow the use of alert, confirm, and prompt + 'no-alert': 2, + // disallow use of arguments.caller or arguments.callee + 'no-caller': 2, + // disallow division operators explicitly at beginning of regular expression + 'no-div-regex': 2, + // disallow else after a return in an if + 'no-else-return': 2, + // disallow Unnecessary Labels + 'no-extra-label': 2, + // disallow comparisons to null without a type-checking operator + 'no-eq-null': 2, + // disallow use of eval() + 'no-eval': 2, + // disallow adding to native types + 'no-extend-native': 2, + // disallow unnecessary function binding + 'no-extra-bind': 2, + // disallow the use of leading or trailing decimal points in numeric literals + 'no-floating-decimal': 2, + // disallow assignments to native objects or read-only global variables + 'no-global-assign': 2, + // disallow use of eval()-like methods + 'no-implied-eval': 2, + // disallow usage of __iterator__ property + 'no-iterator': 2, + // disallow use of labels for anything other then loops and switches + 'no-labels': [2, { 'allowLoop': true, 'allowSwitch': false }], + // disallow unnecessary nested blocks + 'no-lone-blocks': 2, + // disallow use of multiple spaces + 'no-multi-spaces': [2, {'ignoreEOLComments': true}], + // disallow use of multiline strings + 'no-multi-str': 2, + // disallow use of new operator when not part of the assignment or comparison + 'no-new': 2, + // disallow use of new operator for Function object + 'no-new-func': 2, + // disallows creating new instances of String, Number, and Boolean + 'no-new-wrappers': 2, + // disallow use of (old style) octal literals + 'no-octal': 2, + // disallow use of octal escape sequences in string literals, such as + // var foo = 'Copyright \251'; + 'no-octal-escape': 2, + // disallow usage of __proto__ property + 'no-proto': 2, + // disallow declaring the same variable more then once + 'no-redeclare': [2, { 'builtinGlobals': true }], + // disallow unnecessary return await + 'no-return-await': 2, + // disallow use of `javascript:` urls. + 'no-script-url': 2, + // disallow comparisons where both sides are exactly the same + 'no-self-compare': 2, + // disallow use of comma operator + 'no-sequences': 2, + // restrict what can be thrown as an exception + 'no-throw-literal': 2, + // disallow unmodified conditions of loops + // http://eslint.org/docs/rules/no-unmodified-loop-condition + 'no-unmodified-loop-condition': 2, + // disallow usage of expressions in statement position + 'no-unused-expressions': 2, + // disallow unused labels + 'no-unused-labels': 2, + // Disallow unnecessary escape usage + 'no-useless-escape': 2, + // disallow use of void operator + 'no-void': 2, + // disallow use of the with statement + 'no-with': 2, + // require using Error objects as Promise rejection reasons + 'prefer-promise-reject-errors': 2, + // require use of the second argument for parseInt() + 'radix': 2, + // require immediate function invocation to be wrapped in parentheses + // http://eslint.org/docs/rules/wrap-iife.html + 'wrap-iife': [2, 'outside', { 'functionPrototypeMethods': true }], + // require or disallow Yoda conditions + 'yoda': 2, + + + /** Variables section http://eslint.org/docs/rules/#variables **/ + // disallow the catch clause parameter name being the same as a variable in the outer scope + 'no-catch-shadow': 2, + // disallow deletion of variables + 'no-delete-var': 2, + // disallow var and named functions in global scope + 'no-implicit-globals': 2, + // disallow labels that share a name with a variable + 'no-label-var': 2, + // disallow self assignment + 'no-self-assign': 2, + // disallow shadowing of names such as arguments + 'no-shadow-restricted-names': 2, + // disallow use of undeclared variables unless mentioned in a /*global */ block + 'no-undef': 2, + // disallow declaration of variables that are not used in the code + 'no-unused-vars': [2, { 'vars': 'all', 'args': 'after-used', 'ignoreRestSiblings': true }], + // disallow use of variables before they are defined + 'no-use-before-define': [2, { 'functions': false, 'classes': true }], + + + /** Possible Errors section http://eslint.org/docs/rules/#possible-errors **/ + // enforce “for” loop update clause moving the counter in the right direction. + 'for-direction': 2, + // enforce return statements in getters + 'getter-return': 2, + // disallow comparing against -0. Use Object.is(x, -0) + 'no-compare-neg-zero': 2, + // disallow assignment in conditional expressions + 'no-cond-assign': [2, 'always'], + // disallow use of constant expressions in conditions + 'no-constant-condition': [2, { 'checkLoops': false }], + // disallow control characters in regular expressions + 'no-control-regex': 2, + // disallow duplicate arguments in functions + 'no-dupe-args': 2, + // Creating objects with duplicate keys in objects can cause unexpected behavior in your application + 'no-dupe-keys': 2, + // disallow a duplicate case label. + 'no-duplicate-case': 2, + // disallow the use of empty character classes in regular expressions + 'no-empty-character-class': 2, + // disallow empty statements + 'no-empty': 2, + // disallow assigning to the exception in a catch block + 'no-ex-assign': 2, + // disallow double-negation boolean casts in a boolean context + 'no-extra-boolean-cast': 2, + // disallow unnecessary parentheses. + // if you are not sure about operator precedence, visit that page + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#Table + 'no-extra-parens': [2, 'all', { + 'conditionalAssign': false, + 'returnAssign': false, + 'nestedBinaryExpressions': true, // No parens around (a && b) + 'ignoreJSX': 'multi-line', // Allows extra parentheses around multi-line jsx only + 'enforceForArrowConditionals': true, // No parens around arrow return expression a => (a ? b : c) + }], + // disallow unnecessary semicolons + 'no-extra-semi': 2, + // disallow overwriting functions written as function declarations + 'no-func-assign': 2, + // disallow function or variable declarations in nested blocks + 'no-inner-declarations': 2, + // disallow invalid regular expression strings in the RegExp constructor + 'no-invalid-regexp': 2, + // disallow irregular whitespace outside of strings and comments + 'no-irregular-whitespace': 2, + // disallow the use of object properties of the global object (Math and JSON) as functions + 'no-obj-calls': 2, + // disallow multiple spaces in a regular expression literal + 'no-regex-spaces': 2, + // disallow sparse arrays + 'no-sparse-arrays': 2, + // disallow unreachable statements after a return, throw, continue, or break statement + 'no-unreachable': 2, + // disallow control flow statements in finally blocks + 'no-unsafe-finally': 2, + // disallow negating the left operand of relational operators + 'no-unsafe-negation': 2, + // disallow comparisons with the value NaN + 'use-isnan': 2, + // ensure that the results of typeof are compared against a valid string + 'valid-typeof': 2, + // Avoid code that looks like two expressions but is actually one + 'no-unexpected-multiline': 2, + + /** Stylistic Issues section http://eslint.org/docs/rules/#stylistic-issues **/ + // enforce spacing inside array brackets + 'array-bracket-spacing': [2, 'never'], + // enforce consistent spacing inside single-line blocks + 'block-spacing': [2, 'never'], + // enforce one true brace style + 'brace-style': [2, '1tbs', { 'allowSingleLine': true }], + // require camel case names + 'camelcase': [2, { 'properties': 'never' }], + // enforce spacing before and after comma + 'comma-spacing': [2, { 'before': false, 'after': true }], + // enforce one true comma style + 'comma-style': [2, 'last'], + // disallow padding inside computed properties + 'computed-property-spacing': [2, 'never'], + // enforce newline at the end of file, with no multiple empty lines + 'eol-last': 2, + // require or disallow spacing between function identifiers and their invocations + 'func-call-spacing': 2, + 'indent-legacy':[2, 2, { + 'SwitchCase': 1, + 'VariableDeclarator': {'var': 2, 'let': 2, 'const': 3}, + 'outerIIFEBody': 1, + 'MemberExpression': 1, + 'FunctionDeclaration': {'parameters': 'first', 'body': 1}, + 'FunctionExpression': {'parameters': 'first', 'body': 1}, + 'CallExpression': {'arguments': 1}, + 'ArrayExpression': 1, + 'ObjectExpression': 1, + }], + // specify whether double or single quotes should be used in JSX attributes + 'jsx-quotes': [2, 'prefer-double'], + // enforces spacing between keys and values in object literal properties + 'key-spacing': [2, { 'beforeColon': false, 'afterColon': true }], + // require a space before & after certain keywords + 'keyword-spacing': [2, { + 'before': true, + 'after': true, + 'overrides': { + 'return': { 'after': true }, + 'throw': { 'after': true }, + 'case': { 'after': true }, + } + }], + // enforces empty lines around comments + 'lines-around-comment': [2, { + 'beforeBlockComment': true, + 'afterBlockComment': false, + 'beforeLineComment': false, + 'afterLineComment': false, + 'allowBlockStart': true, + 'allowBlockEnd': true, + 'allowClassStart': true, + 'allowClassEnd': true, + 'allowObjectStart': true, + 'allowObjectEnd': true, + 'allowArrayEnd': true, + 'applyDefaultIgnorePatterns': true, + }], + // require or disallow an empty line between class members + 'lines-between-class-members': [2, 'always', {'exceptAfterSingleLine': true}], + // maximum length of a line in your program + 'max-len': [2, { + 'code': 140, // The character count to use whenever a tab character is encountered + 'comments': 140, // Maximum line length for comments; defaults to value of code + 'tabWidth': 2, // The character count to use whenever a tab character is encountered + 'ignoreUrls': true, // Ignores lines that contain a URL + 'ignoreStrings': true, // Ignores lines that contain a double-quoted or single-quoted string + 'ignoreComments': false, // Ignores all trailing comments and comments on their own line + 'ignoreTrailingComments': true, // Ignores only trailing comments + 'ignoreTemplateLiterals': true, // Ignores lines that contain a template literal + 'ignoreRegExpLiterals': true, // Ignores lines that contain a RegExp literal + }], + // require a capital letter for constructors + 'new-cap': [2, { 'newIsCap': true, 'capIsNew': false }], + // disallow the omission of parentheses when invoking a constructor with no arguments + 'new-parens': 2, + // disallow use of the Array constructor + 'no-array-constructor': 2, + // disallow if as the only statement in an else block + 'no-lonely-if': 2, + // disallow mixed spaces and tabs for indentation + 'no-mixed-spaces-and-tabs': 2, + // disallow multiple empty lines and only one newline at the end + 'no-multiple-empty-lines': [2, { 'max': 2, 'maxEOF': 1, 'maxBOF': 1 }], + // disallow use of the Object constructor + 'no-new-object': 2, + // disallow tabs in file + 'no-tabs': 2, + // disallow trailing whitespace at the end of lines + 'no-trailing-spaces': 2, + // disallow dangling underscores in identifiers + 'no-underscore-dangle': [2, { 'enforceInMethodNames': false, 'allow': [ + '__DEV__', + '__STYLES_FETCH__', + '__WHY_UPDATE__', + '__LOG_TREE__', + '__LOG_ROUTER__', + '__LOG_ACTIONS__', + '__REACT_PERF__', + '_loginHref_', + '_rootComponentInstance_', + ] }], + // disallow the use of Boolean literals in conditional expressions + // also, prefer `a || b` over `a ? a : b` + 'no-unneeded-ternary': [2, { 'defaultAssignment': false }], + // disallow whitespace before properties + 'no-whitespace-before-property': 2, + // enforce the location of single-line statements + 'nonblock-statement-body-position': [2, 'beside'], + // allow just one var statement per function + 'one-var': [2, 'never'], + // require a newline around variable declaration + 'one-var-declaration-per-line': [2, 'always'], + // enforce operators to be placed before or after line breaks + 'operator-linebreak': [2, 'after'], + // enforce padding within blocks + 'padded-blocks': [2, {'blocks': 'never', 'classes': 'never', 'switches': 'never'}], + // require or disallow padding lines between statements + 'padding-line-between-statements': [2, + // Always require blank lines after directive (like 'use-strict'), except between directives + {blankLine: 'always', prev: 'directive', next: '*'}, + {blankLine: 'any', prev: 'directive', next: 'directive'}, + // Always require blank lines after import, except between imports + {blankLine: 'always', prev: 'import', next: '*'}, + {blankLine: 'any', prev: 'import', next: 'import'}, + // Always require blank lines before and after every sequence of variable declarations and export + {blankLine: 'always', prev: '*', next: ['const', 'let', 'var', 'export']}, + {blankLine: 'always', prev: ['const', 'let', 'var', 'export'], next: '*'}, + {blankLine: 'any', prev: ['const', 'let', 'var', 'export'], next: ['const', 'let', 'var', 'export']}, + // Always require blank lines before and after class declaration, if, do/while, switch, try + {blankLine: 'always', prev: '*', next: ['if', 'class', 'for', 'do', 'while', 'switch', 'try']}, + {blankLine: 'always', prev: ['if', 'class', 'for', 'do', 'while', 'switch', 'try'], next: '*'}, + // Always require blank lines before return statements + {blankLine: 'always', prev: '*', next: 'return'}, + ], + // require quotes around object literal property names + 'quote-props': [2, 'as-needed', { 'keywords': false, 'unnecessary': false, 'numbers': false }], + // specify whether double or single quotes should be used + 'quotes': [2, 'single', 'avoid-escape'], + // enforce spacing before and after semicolons + 'semi-spacing': [2, { 'before': false, 'after': true }], + // enforce location of semicolons + 'semi-style': [2, 'last'], + // require use of semicolons where they are valid instead of ASI + 'semi': [2, 'always', {'omitLastInOneLineBlock': false}], + // require or disallow space before blocks + 'space-before-blocks': 2, + // require or disallow space before function opening parenthesis + 'space-before-function-paren': [2, { 'anonymous': 'always', 'named': 'never', 'asyncArrow': 'always' }], + // require or disallow spaces inside parentheses + 'space-in-parens': [2, 'never'], + // require spaces around operators + 'space-infix-ops': 2, + // enforce spacing around colons of switch statements + 'switch-colon-spacing': [2, {'after': true, 'before': false}], + // require or disallow spacing between template tags and their literals + 'template-tag-spacing': [2, 'never'], + // files must not begin with the Unicode Byte Order Mark (BOM) + 'unicode-bom': [2, 'never'], + + /* https://github.com/babel/eslint-plugin-babel */ + // Turn them on as they're needed + 'babel/new-cap': 0, + 'babel/object-curly-spacing': 2, + 'babel/no-invalid-this': 0, + 'babel/semi': 2, + + /** React common section https://github.com/yannickcr/eslint-plugin-react#list-of-supported-rules **/ + // Forbid "button" element without an explicit "type" attribute + 'react/button-has-type': 2, + // Prevent extraneous defaultProps on components + 'react/default-props-match-prop-types': [2, {'allowRequiredDefaults': false}], + // Forbid foreign propTypes + 'react/forbid-foreign-prop-types': 2, + // Prevent using this.state inside this.setState + 'react/no-access-state-in-setstate': 2, + // Prevent using children and the dangerouslySetInnerHTML prop at the same time + 'react/no-danger-with-children': 2, + // Prevent usage of deprecated methods + 'react/no-deprecated': 2, + // Prevent usage of setState in componentDidMount + // Updating the state after a component mount will trigger a second render() call and leads to property/layout thrashing + 'react/no-did-mount-set-state': [2, 'disallow-in-func'], + // Prevent usage of setState in componentDidUpdate + // Updating the state after a component update will trigger a second render() call and leads to property/layout thrashing + 'react/no-did-update-set-state': [2, 'disallow-in-func'], + // Prevent direct mutation of this.state + 'react/no-direct-mutation-state': 2, + // Prevent usage of findDOMNode + // Facebook will eventually deprecate findDOMNode as it blocks certain improvements in React in the future + 'react/no-find-dom-node': 2, + // isMounted is an anti-pattern, is not available when using ES6 classes, and it is on its way to being officially deprecated + 'react/no-is-mounted': 2, + // Prevent multiple component definition per file + 'react/no-multi-comp': [2, { 'ignoreStateless': true }], + // Prevent usage of shouldComponentUpdate when extending React.PureComponent + 'react/no-redundant-should-component-update': 2, + // Prevent usage of the return value of React.render + 'react/no-render-return-value': 2, + // Prevent common casing typos + 'react/no-typos': 2, + // Prevent using string references + 'react/no-string-refs': 2, + // Prevent usage of unknown DOM property + 'react/no-unknown-property': 2, + // Prevent usage of setState in componentWillUpdate + // Updating the state during the componentWillUpdate step can lead to indeterminate component state and is not allowed + 'react/no-will-update-set-state': 2, + // Require stateless functions when not using lifecycle methods, setState or ref + 'react/prefer-stateless-function': [2, { 'ignorePureComponents': true }], + // Enforce ES5 or ES6 class for returning value in render function + 'react/require-render-return': 2, + // Prevent extra closing tags for components without children + 'react/self-closing-comp': [2, { 'component': true, 'html': true }], + // Enforce component methods order + 'react/sort-comp': [2, { + 'order': [ + 'static-methods', + 'lifecycle', + '/^on.+$/', + '/^(get|set)(?!(InitialState$|DefaultProps$|ChildContext$)).+$/', + 'setters', + 'getters', + 'everything-else', + '/^render.+$/', + 'render' + ] + }], + // Enforce style prop value being an object + 'react/style-prop-object': 2, + // Prevent void DOM elements (e.g. ,
) from receiving children + 'react/void-dom-elements-no-children': 2, + + /** React JSX section https://github.com/yannickcr/eslint-plugin-react#jsx-specific-rules **/ + // Enforce boolean attributes notation in JSX + 'react/jsx-boolean-value': [2, 'never'], + // Validate closing tag location in JSX + 'react/jsx-closing-tag-location': 2, + // Enforce or disallow spaces inside of curly braces in JSX attributes + 'react/jsx-curly-spacing': [2, { + 'when': 'never', + 'children': true, + 'attributes': true, + 'allowMultiline': true, + 'spacing': {'objectLiterals': 'never'} + }], + // Enforce or disallow spaces around equal signs in JSX attributes + 'react/jsx-equals-spacing': [2, 'never'], + // Restrict file extensions that may contain JSX + 'react/jsx-filename-extension': [2, { "extensions": ['.js', '.jsx'] }], + // Enforce event handler naming conventions in JSX + 'react/jsx-handler-names': [2, { + 'eventHandlerPrefix': 'handle', + 'eventHandlerPropPrefix': 'on', + }], + // Validate JSX indentation. + 'react/jsx-indent': [2, 2], + // Prevent usage of .bind() and arrow functions in JSX props + // A bind call or arrow function in a JSX prop creates a brand new function on every single render + // This is bad for performance and cause unnecessary re-renders if child component is pure or contains shouldComponentUpdate hook + 'react/jsx-no-bind': [2, { + 'ignoreRefs': false, + 'allowArrowFunctions': false, + 'allowBind': false, + }], + // Prevent comments from being inserted as text nodes + 'react/jsx-no-comment-textnodes': 2, + // Creating JSX elements with duplicate props can cause unexpected behavior in your application + 'react/jsx-no-duplicate-props': [2, { 'ignoreCase': true }], + // Enforce curly braces or disallow unnecessary curly braces in JSX + 'react/jsx-curly-brace-presence': [2, {props: 'never', children: 'never'}], + // Disallow undeclared variables in JSX + 'react/jsx-no-undef': 2, + // One JSX Element Per Line + 'react/jsx-pascal-case': 2, + // Validate whitespace in and around the JSX opening and closing bracket + 'react/jsx-tag-spacing': [2, { + 'closingSlash': 'never', + 'beforeSelfClosing': 'never', + 'afterOpening':'never' + }], + // Prevent React to be incorrectly marked as unused + 'react/jsx-uses-react': 2, + // Prevent variables used in JSX to be incorrectly marked as unused + 'react/jsx-uses-vars': 2, + + /** Import Static analysis https://github.com/benmosher/eslint-plugin-import#rules **/ + // Ensure imports point to a file/module that can be resolved + 'import/no-unresolved': [2, { caseSensitive: true, commonjs: true, amd: false }], + // Ensure named imports correspond to a named export in the remote file + 'import/named': 2, + // Ensure a default export is present, given a default import + 'import/default': 2, + // Ensure imported namespaces contain dereferenced properties as they are dereferenced + 'import/namespace': [2, { allowComputed: true }], + // Forbid import of modules using absolute paths + 'import/no-absolute-path': [2, { esmodule: true, commonjs: true, amd: false }], + + /** Import Helpful warnings https://github.com/benmosher/eslint-plugin-import#rules **/ + // Report any invalid exports, i.e. re-export of the same name + 'import/export': 2, + // Report use of exported name as property of default export + 'import/no-named-as-default-member': 2, + // Forbid the use of extraneous packages, allow devDependencies + 'import/no-extraneous-dependencies': [2, { devDependencies: true, packageDir: path.resolve('./') }], + // Forbid the use of mutable exports with var or let + 'import/no-mutable-exports': 2, + + /** Import Style guide https://github.com/benmosher/eslint-plugin-import#rules **/ + // Ensure all imports appear before other statements + 'import/first': 2, + // Report repeated import of the same module in multiple places + 'import/no-duplicates': 2, + // Ensure consistent use of file extension within the import path. Use extensions except .js/jsx + 'import/extensions': [2, 'always', { js: 'never', jsx: 'never' }], + } +}; diff --git a/.gitignore b/.gitignore index 54b67f0..ac8995a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ Thumbs.db node_modules/ jspm_packages/ +# dist folder, users can get build files on unpkg +dist/ + # Optional npm cache directory .npm @@ -23,12 +26,6 @@ jspm_packages/ # Directory for instrumented libs generated by jscoverage/JSCover lib-cov -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - # Optional eslint cache .eslintcache diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..43c97e7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f6b462c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,4 @@ +## Change Log + +### 1.0.0 (2018-03-15) +- Initial release (@klimashkin) \ No newline at end of file diff --git a/README.md b/README.md index c6351d6..df41caa 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -# react-responsive-container -Leverage ResizeObserver to add breakpoints to your component +# react-size-watcher +Leverage ResizeObserver by adding breakpoints to your component diff --git a/package.json b/package.json new file mode 100644 index 0000000..f1066ce --- /dev/null +++ b/package.json @@ -0,0 +1,82 @@ +{ + "name": "react-size-watcher", + "version": "1.0.0", + "author": "Pavel Klimashkin", + "description": "Leverage ResizeObserver to add breakpoints to your component and track its size", + "homepage": "https://github.com/klimashkin/react-size-watcher", + "repository": { + "type": "git", + "url": "git+https://github.com/klimashkin/react-size-watcher.git" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/klimashkin/react-size-watcher/issues" + }, + "keywords": [ + "react", + "responsive", + "resize", + "resizeobserver", + "ResizeObserver", + "react-size-watcher", + "react-size-observer", + "media query" + ], + "main": "./dist/react-size-watcher.js", + "es2015": "./dist/es2015/react-size-watcher.js", + "es2017": "./dist/es2017/react-size-watcher.js", + "files": [ + "dist", + "src" + ], + "peerDependencies": { + "react": "^16.2.0" + }, + "dependencies": { + "prop-types": "^15.6.1" + }, + "devDependencies": { + "babel-cli": "^6.26.0", + "babel-core": "^6.26.0", + "babel-eslint": "^8.2.2", + "babel-loader": "^7.1.4", + "babel-plugin-syntax-async-functions": "6.13.0", + "babel-plugin-syntax-trailing-function-commas": "6.22.0", + "babel-plugin-transform-async-to-generator": "6.24.1", + "babel-plugin-transform-class-properties": "^6.24.1", + "babel-plugin-transform-object-rest-spread": "^6.26.0", + "babel-plugin-transform-react-remove-prop-types": "0.4.13", + "babel-preset-env": "^1.6.1", + "babel-preset-react": "^6.24.1", + "cross-env": "^5.1.4", + "eslint": "^4.18.2", + "eslint-plugin-babel": "^4.1.2", + "eslint-plugin-import": "^2.9.0", + "eslint-plugin-react": "^7.7.0", + "github-changes": "^1.1.2", + "null-loader": "0.1.1", + "react": "^16.2.0", + "react-dom": "^16.2.0", + "rimraf": "2.6.2", + "uglifyjs-webpack-plugin": "1.2.3", + "webpack": "^4.1.1", + "webpack-cli": "2.0.12" + }, + "scripts": { + "preversion": "npm run lint && npm run clean && npm run build", + "postversion": "npm run changelog", + "build:umd": "cross-env BUILD_MODE=umd webpack", + "build:umd-min": "cross-env BUILD_MODE=umd-min webpack", + "build:es6": "cross-env BUILD_MODE=es2015 webpack", + "build:es6-min": "cross-env BUILD_MODE=es2015-min webpack", + "build:es8": "cross-env BUILD_MODE=es2017 webpack", + "build:es8-min": "cross-env BUILD_MODE=es2017-min webpack", + "build": "npm run build:umd && npm run build:umd-min && npm run build:es6 && npm run build:es6-min && npm run build:es8 && npm run build:es8-min", + "clean": "rimraf dist", + "lint": "eslint ./", + "changelog": "github-changes -o klimashkin -r react-size-watcher -b master -f ./CHANGELOG.md --order-semver --use-commit-body" + }, + "engines": { + "node": ">=6.0.0" + } +} diff --git a/src/SizeWatcher.js b/src/SizeWatcher.js new file mode 100644 index 0000000..0393d17 --- /dev/null +++ b/src/SizeWatcher.js @@ -0,0 +1,245 @@ +import PropTypes from 'prop-types'; +import {Component, createElement} from 'react'; +import {contextTypes} from './SizeWatcherTypes'; + +const defaultBreakpoint = {props: {}, minWidth: 0, maxWidth: Infinity, minHeight: 0, maxHeight: Infinity}; + +export default class SizeWatcher extends Component { + static contextTypes = contextTypes; + + static propTypes = { + // Array of breakpoints + breakpoints: PropTypes.arrayOf(PropTypes.shape({ + // Dimensions if breakpoint + minWidth: PropTypes.number, + maxWidth: PropTypes.number, + minHeight: PropTypes.number, + maxHeight: PropTypes.number, + // Props that are mixed into container props + props: PropTypes.object, + // Property that contains any custom data useful in child render function + data: PropTypes.any, + })).isRequired, + + // Breakpoint match strategy + // 'order' (default) - match by breakpoints order, like in css @media-queries, when the last match wins + // 'breakpointArea' - match by breakpoints area, when the smallest area is considered to be more specific + // 'intersectionArea' - match by area of breakpoint/size intersection, + // when the biggest intersection area is considered to be more specific + matchBy: PropTypes.oneOf(['order', 'breakpointArea', 'intersectionArea']), + + // By default only container will be rendered on first render() call (because usually it's a block element) + // to compute its width and decide what breakpoint pass down to render function. + // But if container is a part of flex, or grow according to its content, or breakpoint depend on height, + // content should be rendered with visibility:hidden before breakpoint computation. + renderContentOnInit: PropTypes.bool, // By default - false + // If renderContentOnInit is true, pass this flag to show content on first render before computation + showOnInit: PropTypes.bool, // By default - false + + // Container can mimic any html element ('div', 'h2' etc) or custom component (constructors like Link, Button etc) + type: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), + + // Render function, that is called on breakpoint change + children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired, + + // Optional callback on each size change + onSizeChange: PropTypes.func, + }; + + static defaultProps = { + renderContentOnInit: false, + showOnInit: false, + matchBy: 'order', + type: 'div', + }; + + constructor(props, context) { + super(props, context); + + this.processBreakpoints(props.breakpoints); + this.state = { + breakpoint: props.renderContentOnInit ? this.findBreakpoint() : null, + }; + + this.saveRef = this.saveRef.bind(this); + } + + componentWillReceiveProps(nextProps) { + const {props} = this; + + if (nextProps.breakpoints !== props.breakpoints || nextProps.matchBy !== props.matchBy) { + this.processBreakpoints(nextProps.breakpoints); + + const breakpoint = this.findBreakpoint(this.size); + + if (breakpoint !== this.state.breakpoint) { + this.setState({breakpoint}); + } + } + } + + componentWillUnmount() { + this.checkOut(); + } + + saveRef(dom) { + if (dom) { + // If type props contains custom react components, ref returns instance of that component + // Try taking dom by calling method getWrappedInstance on it + while (typeof dom.getWrappedInstance === 'function') { + dom = dom.getWrappedInstance(); + } + + this.checkIn(dom); + } else { + this.checkOut(); + } + } + + checkIn(dom) { + this.dom = dom; + this.context.sizeWatcher.checkIn(this, dom); + } + + checkOut() { + if (this.dom) { + this.context.sizeWatcher.checkOut(this.dom); + this.dom = null; + } + } + + updateSize(size) { + if (this.size === undefined || size.width !== this.size.width || size.height !== this.size.height) { + const breakpointNeedCheck = + this.size === undefined || + this.sensitiveToWidth && size.width !== this.size.width || + this.sensitiveToHeight && size.height !== this.size.height; + + const currentBreakpoint = this.state.breakpoint; + const newBreakpoint = breakpointNeedCheck ? this.findBreakpoint(size) : currentBreakpoint; + + this.size = size; + + if (typeof this.props.onSizeChange === 'function') { + this.props.onSizeChange(size, currentBreakpoint, newBreakpoint); + } + + if (newBreakpoint !== currentBreakpoint) { + this.setState({breakpoint: newBreakpoint}); + } + } + } + + processBreakpoints(breakpoints) { + this.sensitiveToWidth = false; + this.sensitiveToHeight = false; + this.breakpoints = breakpoints; + + for (const {minWidth, maxWidth, minHeight, maxHeight} of breakpoints) { + // Validate maxWidth/minWidth in development + const maxWidthType = typeof maxWidth; + const minWidthType = typeof minWidth; + const maxHeightType = typeof maxHeight; + const minHeightType = typeof minHeight; + + if (maxWidthType !== 'undefined' && (maxWidthType !== 'number' || maxWidth < 0) || + minWidthType !== 'undefined' && (minWidthType !== 'number' || minWidth < 0 || maxWidth && minWidth > maxWidth)) { + throw Error(`Wrong min ${JSON.stringify(minWidth)} or max ${JSON.stringify(maxWidth)} width in breakpoint`); + } + + if (maxHeightType !== 'undefined' && (maxHeightType !== 'number' || maxHeight < 0) || + minHeightType !== 'undefined' && (minHeightType !== 'number' || minHeight < 0 || maxHeight && minHeight > maxHeight)) { + throw Error(`Wrong min ${JSON.stringify(minHeight)} or max ${JSON.stringify(maxHeight)} height in breakpoint`); + } + + if (!this.sensitiveToWidth && (minWidth || maxWidth)) { + this.sensitiveToWidth = true; + } + + if (!this.sensitiveToHeight && (minHeight || maxHeight)) { + this.sensitiveToHeight = true; + } + } + + if (this.props.matchBy === 'order') { + this.findBreakpoint = this.findBreakpointByOrder; + } else if (this.props.matchBy === 'breakpointArea') { + this.findBreakpoint = this.findBreakpointByArea; + } else if (this.props.matchBy === 'intersectionArea') { + this.findBreakpoint = this.findBreakpointByIntersection; + } + } + + // Find the last breakpoint that matches given size + findBreakpointByOrder({width = Infinity, height = Infinity} = {}) { + for (let i = this.breakpoints.length; i--;) { + const breakpoint = this.breakpoints[i]; + const {minWidth = 0, maxWidth = Infinity, minHeight = 0, maxHeight = Infinity} = breakpoint; + + if (width >= minWidth && width <= maxWidth && height >= minHeight && height <= maxHeight) { + return breakpoint; + } + } + + return defaultBreakpoint; + } + + // Find breakpoint with the smallest area + findBreakpointByArea({width = 1e6, height = 1e6} = {}) { + return this.breakpoints.reduce((result, breakpoint) => { + const {minWidth = 0, maxWidth = 1e6, minHeight = 0, maxHeight = 1e6} = breakpoint; + + if (width >= minWidth && width <= maxWidth && height >= minHeight && height <= maxHeight) { + const area = (maxWidth - minWidth) * (maxHeight - minHeight); + + if (area <= result.area) { + result.area = area; + result.breakpoint = breakpoint; + } + } + + return result; + }, {area: Infinity, breakpoint: defaultBreakpoint}).breakpoint; + } + + // Find breakpoint with the biggest breakpoint/size intersection area + findBreakpointByIntersection({width = 1e6, height = 1e6} = {}) { + return this.breakpoints.reduce((result, breakpoint) => { + const {minWidth = 0, maxWidth = 1e6, minHeight = 0, maxHeight = 1e6} = breakpoint; + const area = Math.max(0, Math.min(maxWidth, width) - minWidth) * Math.max(0, Math.min(maxHeight, height) - minHeight); + + if (area >= result.area) { + result.area = area; + result.breakpoint = breakpoint; + } + + return result; + }, {area: 0, breakpoint: defaultBreakpoint}).breakpoint; + } + + render() { + const { + props: { + type, breakpoints, matchBy, renderContentOnInit, showOnInit, children, onSizeChange, + ...elementProps + }, + state: {breakpoint}, + } = this; + + let renderedChildren; + + if (breakpoint !== null) { + renderedChildren = typeof children === 'function' ? children(breakpoint, this.size) : children; + + Object.assign(elementProps, breakpoint.props); + + if (this.size === undefined && !showOnInit) { + elementProps.style = {...elementProps.style, visibility: 'hidden'}; + } + } + + elementProps.ref = this.saveRef; + + return createElement(type, elementProps, renderedChildren); + } +} diff --git a/src/SizeWatcherProvider.js b/src/SizeWatcherProvider.js new file mode 100644 index 0000000..d43cf56 --- /dev/null +++ b/src/SizeWatcherProvider.js @@ -0,0 +1,63 @@ +import {Component} from 'react'; +import {contextTypes} from './SizeWatcherTypes'; + +export default class SizeWatcherProvider extends Component { + static childContextTypes = contextTypes; + + constructor(props, context) { + super(props, context); + + this.checkInChildContainer = this.checkInChildContainer.bind(this); + this.checkOutChildContainer = this.checkOutChildContainer.bind(this); + + this.childContext = { + sizeWatcher: { + checkIn: this.checkInChildContainer, + checkOut: this.checkOutChildContainer, + }, + }; + + // Map: {[]: {instance, dom: }} + this.childrenContainers = new Map(); + } + + getChildContext() { + return this.childContext; + } + + componentDidMount() { + // Create resizeObservable that handles all children containers size change + // One for all: https://groups.google.com/a/chromium.org/forum/#!msg/blink-dev/z6ienONUb5A/F5-VcUZtBAAJ + this.resizeObservable = new ResizeObserver(entries => { + for (const {target, contentRect} of entries) { + const container = this.childrenContainers.get(target); + + if (container) { + container.instance.updateSize({width: contentRect.width, height: contentRect.height}); + } + } + }); + + // Observe each checkedIn element in a loop. + // Observer callback will be called once on the next requestAnimationFrame with all elements in `entries` array + this.childrenContainers.forEach(({dom}) => { + this.resizeObservable.observe(dom); + }); + } + + checkInChildContainer(instance, dom) { + this.childrenContainers.set(dom, {instance, dom}); + + if (this.resizeObservable) { + this.resizeObservable.observe(dom); + } + } + + checkOutChildContainer(dom) { + this.childrenContainers.delete(dom); + } + + render() { + return this.props.children; + } +} diff --git a/src/SizeWatcherTypes.js b/src/SizeWatcherTypes.js new file mode 100644 index 0000000..1a91e89 --- /dev/null +++ b/src/SizeWatcherTypes.js @@ -0,0 +1,8 @@ +import PropTypes from 'prop-types'; + +export const contextTypes = { + sizeWatcher: PropTypes.shape({ + checkIn: PropTypes.func, + checkOut: PropTypes.func, + }), +}; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..f39fd5a --- /dev/null +++ b/src/index.js @@ -0,0 +1,6 @@ +import SizeWatcher from './SizeWatcher'; +import * as SizeWatcherTypes from './SizeWatcherTypes'; + +export {SizeWatcherTypes}; +export default SizeWatcher; +export {default as SizeWatcherProvider} from './SizeWatcherProvider'; diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..b62ea02 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,125 @@ +const path = require('path'); + +let babelOptions; +let filename = '[name].js'; +const minify = process.env.BUILD_MODE.endsWith('min'); + +if (!process.env.BUILD_MODE.startsWith('umd')) { + // To see es* prefix in webpack stat output, concatenate folder with filename + filename = `${process.env.BUILD_MODE.match(/^([^-]+)/)[1]}/${filename}`; +} + +if (process.env.BUILD_MODE.startsWith('umd')) { + babelOptions = { + presets: [ + 'react', + 'env', + ], + plugins: [ + 'transform-class-properties', + 'transform-object-rest-spread', + ['transform-react-remove-prop-types', {mode: 'remove'}], + ], + }; +} else if (process.env.BUILD_MODE.startsWith('es2015')) { + babelOptions = { + plugins: [ + ['transform-react-jsx', {useBuiltIns: true}], + 'syntax-jsx', + + 'transform-async-to-generator', + + 'transform-class-properties', + ['transform-object-rest-spread', {useBuiltIns: true}], + ['transform-react-remove-prop-types', {mode: 'remove'}], + ], + }; +} else if (process.env.BUILD_MODE.startsWith('es2017')) { + babelOptions = { + plugins: [ + ['transform-react-jsx', {useBuiltIns: true}], + 'syntax-jsx', + + 'transform-class-properties', + ['transform-object-rest-spread', {useBuiltIns: true}], + ['transform-react-remove-prop-types', {mode: 'remove'}], + ], + }; +} + +module.exports = { + entry: { + [`react-size-watcher${minify ? '.min' : ''}`]: './src/index.js', + }, + output: { + filename, + sourceMapFilename: `${filename}.map`, + path: path.resolve(__dirname, 'dist'), + pathinfo: false, + libraryTarget: 'umd', + library: 'SizeWatcher', + }, + devtool: 'source-map', + mode: 'production', + optimization: { + removeAvailableModules: true, + removeEmptyChunks: true, + mergeDuplicateChunks: true, + flagIncludedChunks: true, + occurrenceOrder: true, + providedExports: true, + usedExports: true, + sideEffects: true, + concatenateModules: true, + splitChunks: false, + runtimeChunk: false, + noEmitOnErrors: true, + namedModules: true, + namedChunks: true, + nodeEnv: 'production', + minimize: minify, + }, + resolve: { + modules: [ + path.resolve('src'), + 'node_modules', + ], + }, + externals: { + 'react': 'umd react', + 'prop-types': { + amd: 'prop-types', + root: 'PropTypes', + commonjs: 'prop-types', + commonjs2: 'prop-types', + }, + }, + module: { + rules: [ + { + test: /\.js$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', + options: babelOptions, + }, + }, + ], + }, + node: { + process: false, + setImmediate: false, + }, + stats: { + assets: true, + colors: true, + errors: true, + errorDetails: true, + hash: false, + timings: true, + version: true, + warnings: true, + entrypoints: false, + modules: false, + }, +};