├── .eslintrc ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── bower.json ├── docs ├── Makefile ├── _static │ └── overrides.css ├── _templates │ └── layout.html ├── api.rst ├── conf.py ├── examples │ ├── custom-column-formatters.rst │ ├── datagrid.rst │ ├── index.rst │ ├── locked-columns.rst │ ├── selectable-cells.rst │ ├── selectable-rows.rst │ └── sortable-grid.rst ├── index.rst └── styling.rst ├── lib ├── Canvas.js ├── Cell.js ├── ColumnMetrics.js ├── DOMMetrics.js ├── Draggable.js ├── Grid.js ├── Header.js ├── HeaderCell.js ├── HeaderRow.js ├── Row.js ├── ScrollShim.js ├── Viewport.js ├── copyProperties.js ├── emptyFunction.js ├── getScrollbarSize.js ├── getWindowSize.js ├── index.js ├── invariant.js ├── shallowCloneObject.js └── shallowEqual.js ├── package.json ├── standalone ├── Makefile ├── index.js ├── lib └── package.json └── themes └── bootstrap.less /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true 5 | }, 6 | "rules": { 7 | "new-cap": false, 8 | "quotes": false, 9 | "no-underscore-dangle": false, 10 | "no-multi-str": false, 11 | "no-use-before-define": false, 12 | "no-unused-vars": [2, "all"], 13 | "no-mixed-requires": [1, true], 14 | "max-len": [1, 80, 4] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | docs/_build 3 | docs/_static/bundle.js 4 | olddocs/build 5 | standalone/react-grid.min.js 6 | standalone/react-grid.js 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Prometheus Research 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN = ./node_modules/.bin 2 | PATH := $(BIN):$(PATH) 3 | TESTS = $(shell find ./lib -path '**/tests/*.js') 4 | 5 | install: 6 | @npm $@ 7 | 8 | lint: 9 | @eslint-jsx lib/ 10 | 11 | clean: 12 | @rm -rf ./node_modules/ 13 | 14 | test: test-phantomjs 15 | 16 | docs:: 17 | @$(MAKE) --no-print-directory -C docs html 18 | 19 | publish-docs:: 20 | @$(MAKE) --no-print-directory -C docs publish 21 | 22 | ci: 23 | @NODE_PATH=$(NODE_PATH) mochify --watch -R dot $(TESTS) 24 | 25 | test-phantomjs: 26 | @NODE_PATH=$(NODE_PATH) mochify -R spec $(TESTS) 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Grid 2 | 3 | Data grid for [React][]. 4 | 5 | ## Credits 6 | 7 | React Grid is free software created by [Prometheus Research][] and is released 8 | under the MIT. 9 | 10 | [React]: http://facebook.github.io/react/ 11 | [Prometheus Research, LLC]: http://prometheusresearch.com 12 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-grid", 3 | "version": "0.1.0", 4 | "homepage": "https://github.com/prometheusresearch/react-grid", 5 | "authors": [ 6 | "Prometheus Research" 7 | ], 8 | "description": "Grid component for React", 9 | "main": "./lib/index.js", 10 | "moduleType": [ 11 | "node" 12 | ], 13 | "keywords": [ 14 | "react", 15 | "react-component", 16 | "grid" 17 | ], 18 | "license": "MIT", 19 | "ignore": [ 20 | "**/.*", 21 | "node_modules", 22 | "bower_components", 23 | "test", 24 | "tests" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | publish: clean html 50 | @REPO=`git config --local --get remote.origin.url`;\ 51 | (cd ./_build/html;\ 52 | git init;\ 53 | git remote add origin $$REPO;\ 54 | git co -b gh-pages;\ 55 | touch .nojekyll;\ 56 | git add .;\ 57 | git ci -m 'Update docs';\ 58 | git push -f origin gh-pages); 59 | 60 | clean: 61 | rm -rf $(BUILDDIR)/* _static/bundle.js 62 | 63 | _static/bundle.js: $(shell find ../lib -name '*.js') 64 | ../node_modules/.bin/browserify -d \ 65 | -r ../:react-grid \ 66 | -r react/addons:react \ 67 | > $@ 68 | 69 | html: _static/bundle.js 70 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 71 | @echo 72 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 73 | 74 | dirhtml: 75 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 76 | @echo 77 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 78 | 79 | singlehtml: 80 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 81 | @echo 82 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 83 | 84 | pickle: 85 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 86 | @echo 87 | @echo "Build finished; now you can process the pickle files." 88 | 89 | json: 90 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 91 | @echo 92 | @echo "Build finished; now you can process the JSON files." 93 | 94 | htmlhelp: 95 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 96 | @echo 97 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 98 | ".hhp project file in $(BUILDDIR)/htmlhelp." 99 | 100 | qthelp: 101 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 102 | @echo 103 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 104 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 105 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/react-grid.qhcp" 106 | @echo "To view the help file:" 107 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/react-grid.qhc" 108 | 109 | devhelp: 110 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 111 | @echo 112 | @echo "Build finished." 113 | @echo "To view the help file:" 114 | @echo "# mkdir -p $$HOME/.local/share/devhelp/react-grid" 115 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/react-grid" 116 | @echo "# devhelp" 117 | 118 | epub: 119 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 120 | @echo 121 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 122 | 123 | latex: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo 126 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 127 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 128 | "(use \`make latexpdf' here to do that automatically)." 129 | 130 | latexpdf: 131 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 132 | @echo "Running LaTeX files through pdflatex..." 133 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 134 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 135 | 136 | latexpdfja: 137 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 138 | @echo "Running LaTeX files through platex and dvipdfmx..." 139 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 140 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 141 | 142 | text: 143 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 144 | @echo 145 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 146 | 147 | man: 148 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 149 | @echo 150 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 151 | 152 | texinfo: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo 155 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 156 | @echo "Run \`make' in that directory to run these through makeinfo" \ 157 | "(use \`make info' here to do that automatically)." 158 | 159 | info: 160 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 161 | @echo "Running Texinfo files through makeinfo..." 162 | make -C $(BUILDDIR)/texinfo info 163 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 164 | 165 | gettext: 166 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 167 | @echo 168 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 169 | 170 | changes: 171 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 172 | @echo 173 | @echo "The overview file is in $(BUILDDIR)/changes." 174 | 175 | linkcheck: 176 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 177 | @echo 178 | @echo "Link check complete; look for any errors in the above output " \ 179 | "or in $(BUILDDIR)/linkcheck/output.txt." 180 | 181 | doctest: 182 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 183 | @echo "Testing of doctests in the sources finished, look at the " \ 184 | "results in $(BUILDDIR)/doctest/output.txt." 185 | 186 | xml: 187 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 188 | @echo 189 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 190 | 191 | pseudoxml: 192 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 193 | @echo 194 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 195 | -------------------------------------------------------------------------------- /docs/_static/overrides.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | .section { 5 | clear: both; 6 | } 7 | 8 | .reference.internal em { 9 | font-style: normal; 10 | } 11 | 12 | .codeblock, 13 | pre.literal-block, 14 | .rst-content .literal-block, 15 | .rst-content pre.literal-block, 16 | div[class^='highlight'] { 17 | border: none; 18 | border-left: 1px solid #f1f1f1; 19 | } 20 | 21 | code, 22 | .rst-content tt { 23 | border: none; 24 | } 25 | 26 | hr { 27 | border-top: 1px solid #f1f1f1; 28 | } 29 | 30 | .wy-breadcrumbs, 31 | .wy-breadcrumbs + hr, 32 | .wy-breadcrumbs li.wy-breadcrumbs-aside { 33 | display: none; 34 | } 35 | 36 | .wy-nav-top i { 37 | padding: 10px 0; 38 | } 39 | 40 | footer { 41 | font-size: 70%; 42 | text-align: center; 43 | } 44 | footer .rst-footer-buttons { 45 | text-align: left; 46 | } 47 | 48 | .admonition p { 49 | font-size: 90%; 50 | line-height: 1.5em; 51 | } 52 | .admonition .admonition-title { 53 | background: none !important; 54 | color: #9E9D9D !important; 55 | text-transform: uppercase; 56 | padding: 10px; 57 | font-size: 70%; 58 | } 59 | .admonition .admonition-title:before { 60 | content: normal; 61 | } 62 | 63 | @media (min-width: 1100px) { 64 | .admonition.inline { 65 | float: right; 66 | margin-right: -50px; 67 | margin-left: 20px; 68 | width: 40%; 69 | } 70 | } 71 | 72 | .react-grid-Grid { 73 | border: 1px solid #ccc; 74 | font-size: 90%; 75 | margin-bottom: 35px; 76 | } 77 | 78 | .react-grid-Header { 79 | border-bottom: 1px solid #ccc; 80 | background: #eee; 81 | font-weight: bold; 82 | } 83 | 84 | .react-grid-Canvas { 85 | background: #fff; 86 | } 87 | 88 | .react-grid-Cell, 89 | .react-grid-HeaderCell { 90 | padding: 7px; 91 | } 92 | 93 | .react-grid-Cell { 94 | border-bottom: 1px solid #eee; 95 | } 96 | .react-grid-Row:last-child .react-grid-Cell { 97 | border-bottom: none; 98 | } 99 | .react-grid-Row:hover .react-grid-Cell { 100 | background: #eee; 101 | } 102 | 103 | .react-grid-Cell { 104 | background: #fff; 105 | } 106 | 107 | .react-grid-Cell--locked:last-of-type { 108 | border-right: 1px solid #eee; 109 | } 110 | 111 | .react-grid-HeaderCell { 112 | border-right: 1px solid #ccc; 113 | background: #eee; 114 | } 115 | 116 | .react-grid-HeaderCell__resizeHandle:hover { 117 | cursor: ew-resize; 118 | background: #ddd; 119 | } 120 | 121 | .react-grid-HeaderCell--resizing .react-grid-HeaderCell__resizeHandle { 122 | background: #ddd; 123 | } 124 | 125 | -------------------------------------------------------------------------------- /docs/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% set css_files = css_files + ['_static/overrides.css'] %} 2 | {% set script_files = script_files + ['_static/bundle.js'] %} 3 | {% extends "!layout.html" %} 4 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API reference 2 | ============= 3 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # react-grid documentation build configuration file, created by 4 | # sphinx-quickstart on Wed May 21 02:47:06 2014. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | import sphinx_rtd_theme 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | #sys.path.insert(0, os.path.abspath('.')) 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | #needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = ['sphinxcontrib.jsx'] 33 | 34 | # Add any paths that contain templates here, relative to this directory. 35 | templates_path = ['_templates'] 36 | 37 | # The suffix of source filenames. 38 | source_suffix = '.rst' 39 | 40 | # The encoding of source files. 41 | #source_encoding = 'utf-8-sig' 42 | 43 | # The master toctree document. 44 | master_doc = 'index' 45 | 46 | # General information about the project. 47 | project = u'react-grid' 48 | copyright = u'2014, Prometheus Research, LLC' 49 | 50 | # The version info for the project you're documenting, acts as replacement for 51 | # |version| and |release|, also used in various other places throughout the 52 | # built documents. 53 | # 54 | # The short X.Y version. 55 | version = '0.1.0' 56 | # The full version, including alpha/beta/rc tags. 57 | release = '0.1.0' 58 | 59 | # The language for content autogenerated by Sphinx. Refer to documentation 60 | # for a list of supported languages. 61 | #language = None 62 | 63 | # There are two options for replacing |today|: either, you set today to some 64 | # non-false value, then it is used: 65 | #today = '' 66 | # Else, today_fmt is used as the format for a strftime call. 67 | #today_fmt = '%B %d, %Y' 68 | 69 | # List of patterns, relative to source directory, that match files and 70 | # directories to ignore when looking for source files. 71 | exclude_patterns = ['_build'] 72 | 73 | # The reST default role (used for this markup: `text`) to use for all 74 | # documents. 75 | #default_role = None 76 | 77 | # If true, '()' will be appended to :func: etc. cross-reference text. 78 | #add_function_parentheses = True 79 | 80 | # If true, the current module name will be prepended to all description 81 | # unit titles (such as .. function::). 82 | #add_module_names = True 83 | 84 | # If true, sectionauthor and moduleauthor directives will be shown in the 85 | # output. They are ignored by default. 86 | #show_authors = False 87 | 88 | # The name of the Pygments (syntax highlighting) style to use. 89 | pygments_style = 'sphinx' 90 | 91 | # A list of ignored prefixes for module index sorting. 92 | #modindex_common_prefix = [] 93 | 94 | # If true, keep warnings as "system message" paragraphs in the built documents. 95 | #keep_warnings = False 96 | 97 | 98 | # -- Options for HTML output ---------------------------------------------- 99 | 100 | # The theme to use for HTML and HTML Help pages. See the documentation for 101 | # a list of builtin themes. 102 | html_theme = 'sphinx_rtd_theme' 103 | 104 | # Theme options are theme-specific and customize the look and feel of a theme 105 | # further. For a list of options available for each theme, see the 106 | # documentation. 107 | #html_theme_options = {} 108 | 109 | # Add any paths that contain custom themes here, relative to this directory. 110 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 111 | 112 | # The name for this set of Sphinx documents. If None, it defaults to 113 | # " v documentation". 114 | #html_title = None 115 | 116 | # A shorter title for the navigation bar. Default is the same as html_title. 117 | #html_short_title = None 118 | 119 | # The name of an image file (relative to this directory) to place at the top 120 | # of the sidebar. 121 | #html_logo = None 122 | 123 | # The name of an image file (within the static path) to use as favicon of the 124 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 125 | # pixels large. 126 | #html_favicon = None 127 | 128 | # Add any paths that contain custom static files (such as style sheets) here, 129 | # relative to this directory. They are copied after the builtin static files, 130 | # so a file named "default.css" will overwrite the builtin "default.css". 131 | html_static_path = ['_static'] 132 | 133 | # Add any extra paths that contain custom files (such as robots.txt or 134 | # .htaccess) here, relative to this directory. These files are copied 135 | # directly to the root of the documentation. 136 | #html_extra_path = [] 137 | 138 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 139 | # using the given strftime format. 140 | #html_last_updated_fmt = '%b %d, %Y' 141 | 142 | # If true, SmartyPants will be used to convert quotes and dashes to 143 | # typographically correct entities. 144 | #html_use_smartypants = True 145 | 146 | # Custom sidebar templates, maps document names to template names. 147 | #html_sidebars = {} 148 | 149 | # Additional templates that should be rendered to pages, maps page names to 150 | # template names. 151 | #html_additional_pages = {} 152 | 153 | # If false, no module index is generated. 154 | #html_domain_indices = True 155 | 156 | # If false, no index is generated. 157 | #html_use_index = True 158 | 159 | # If true, the index is split into individual pages for each letter. 160 | #html_split_index = False 161 | 162 | # If true, links to the reST sources are added to the pages. 163 | #html_show_sourcelink = True 164 | 165 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 166 | #html_show_sphinx = True 167 | 168 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 169 | #html_show_copyright = True 170 | 171 | # If true, an OpenSearch description file will be output, and all pages will 172 | # contain a tag referring to it. The value of this option must be the 173 | # base URL from which the finished HTML is served. 174 | #html_use_opensearch = '' 175 | 176 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 177 | #html_file_suffix = None 178 | 179 | # Output file base name for HTML help builder. 180 | htmlhelp_basename = 'react-grid-doc' 181 | 182 | 183 | # -- Options for LaTeX output --------------------------------------------- 184 | 185 | latex_elements = { 186 | # The paper size ('letterpaper' or 'a4paper'). 187 | #'papersize': 'letterpaper', 188 | 189 | # The font size ('10pt', '11pt' or '12pt'). 190 | #'pointsize': '10pt', 191 | 192 | # Additional stuff for the LaTeX preamble. 193 | #'preamble': '', 194 | } 195 | 196 | # Grouping the document tree into LaTeX files. List of tuples 197 | # (source start file, target name, title, 198 | # author, documentclass [howto, manual, or own class]). 199 | latex_documents = [ 200 | ('index', 'react-grid.tex', u'React Grid Documentation', 201 | u'Prometheus Research, LCC', 'manual'), 202 | ] 203 | 204 | # The name of an image file (relative to this directory) to place at the top of 205 | # the title page. 206 | #latex_logo = None 207 | 208 | # For "manual" documents, if this is true, then toplevel headings are parts, 209 | # not chapters. 210 | #latex_use_parts = False 211 | 212 | # If true, show page references after internal links. 213 | #latex_show_pagerefs = False 214 | 215 | # If true, show URL addresses after external links. 216 | #latex_show_urls = False 217 | 218 | # Documents to append as an appendix to all manuals. 219 | #latex_appendices = [] 220 | 221 | # If false, no module index is generated. 222 | #latex_domain_indices = True 223 | 224 | 225 | # -- Options for manual page output --------------------------------------- 226 | 227 | # One entry per manual page. List of tuples 228 | # (source start file, name, description, authors, manual section). 229 | man_pages = [ 230 | ('index', 'react-grid', u'React Grid Documentation', 231 | [u'Prometheus Research, LLC'], 1) 232 | ] 233 | 234 | # If true, show URL addresses after external links. 235 | #man_show_urls = False 236 | 237 | 238 | # -- Options for Texinfo output ------------------------------------------- 239 | 240 | # Grouping the document tree into Texinfo files. List of tuples 241 | # (source start file, target name, title, author, 242 | # dir menu entry, description, category) 243 | texinfo_documents = [ 244 | ('index', 'react-grid', u'React Grid Documentation', 245 | u'Prometheus Research, LLC', 'react-grid', 'One line description of project.', 246 | 'Miscellaneous'), 247 | ] 248 | 249 | # Documents to append as an appendix to all manuals. 250 | #texinfo_appendices = [] 251 | 252 | # If false, no module index is generated. 253 | #texinfo_domain_indices = True 254 | 255 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 256 | #texinfo_show_urls = 'footnote' 257 | 258 | # If true, do not generate a @detailmenu in the "Top" node's menu. 259 | #texinfo_no_detailmenu = False 260 | -------------------------------------------------------------------------------- /docs/examples/custom-column-formatters.rst: -------------------------------------------------------------------------------- 1 | Custom column formatters 2 | ======================== 3 | 4 | .. jsx:: 5 | :hidesource: 6 | 7 | var React = require('react') 8 | var ReactGrid = require('react-grid') 9 | 10 | .. jsx:: 11 | 12 | var FancyCell = React.createClass({ 13 | render: function() { 14 | return ( 15 |
16 | № {this.props.value} 17 |
18 | ); 19 | } 20 | }); 21 | 22 | .. jsx:: 23 | 24 | var columns = [ 25 | { 26 | name: '№', 27 | width: '20%', 28 | key: 0, 29 | formatter: FancyCell 30 | }, 31 | { 32 | name: 'Name', 33 | width: '30%', 34 | key: 1 35 | }, 36 | { 37 | name: 'Surname', 38 | width: '50%', 39 | key: 2 40 | } 41 | ]; 42 | 43 | .. jsx:: 44 | :hidesource: 45 | 46 | function rows(start, end) { 47 | var rows = []; 48 | for (var i = start; i < end; i++) { 49 | rows.push([i, 'Name ' + i, 'Surname ' + i]); 50 | } 51 | return rows; 52 | } 53 | 54 | 55 | React.renderComponent( 56 | , 61 | document.getElementById('example')); 62 | 63 | .. raw:: html 64 | 65 |
66 | -------------------------------------------------------------------------------- /docs/examples/datagrid.rst: -------------------------------------------------------------------------------- 1 | Datagrid 2 | ======== 3 | 4 | .. jsx:: 5 | :hidesource: 6 | 7 | var React = require('react') 8 | var ReactGrid = require('react-grid') 9 | 10 | .. jsx:: 11 | 12 | var LoadingIndicator = React.createClass({ 13 | 14 | style: { 15 | textAlign: 'center', 16 | height: '100%', 17 | padding: '8px' 18 | }, 19 | 20 | render: function() { 21 | return ( 22 |
23 | Loading... 24 |
25 | ); 26 | } 27 | }); 28 | 29 | .. jsx:: 30 | 31 | var DataGrid = React.createClass({ 32 | 33 | getInitialState: function() { 34 | return { 35 | rows: [], 36 | hasNext: true 37 | }; 38 | }, 39 | 40 | getDefaultProps: function() { 41 | return { 42 | rowsPerPage: 30, 43 | artificialDelay: 1000 44 | }; 45 | }, 46 | 47 | render: function() { 48 | return this.transferPropsTo( 49 | 53 | ); 54 | }, 55 | 56 | componentDidMount: function() { 57 | this.fetchNextPage(); 58 | }, 59 | 60 | componentWillMount: function() { 61 | this._fetchInProgress = {}; 62 | }, 63 | 64 | rows: function(start, end) { 65 | var rows = this.state.rows.slice(start, end); 66 | 67 | if (this.state.hasNext && this.state.rows.length - end <= 0) { 68 | rows.push(LoadingIndicator()); 69 | } 70 | 71 | if (end >= (this.state.rows.length - 5)) { 72 | this.fetchNextPage(); 73 | } 74 | 75 | return rows; 76 | }, 77 | 78 | fetchNextPage: function() { 79 | if (this.state.hasNext) { 80 | var length = this.state.rows.length; 81 | this.fetchPage(length, length + this.props.rowsPerPage); 82 | } 83 | }, 84 | 85 | fetchPage: function(start, end) { 86 | var key = start + '-' + end; 87 | 88 | if (this._fetchInProgress[key]) { 89 | return; 90 | } 91 | 92 | this._fetchInProgress[key] = true; 93 | 94 | $.ajax({url: this.props.url, dataType: 'json'}) 95 | .then(function(data) { 96 | data = data.slice(start, end); 97 | var rows = this.state.rows.slice(0); 98 | 99 | for (var i = 0, len = data.length; i < len; i++) { 100 | rows[start + i] = data[i]; 101 | } 102 | 103 | var hasNext = data.length === end - start; 104 | 105 | setTimeout(function() { 106 | if (this.isMounted()) { 107 | this.setState({hasNext: hasNext, length: rows.length, rows: rows}, function() { 108 | this._fetchInProgress[key] = false; 109 | }.bind(this)); 110 | } 111 | }.bind(this), this.props.artificialDelay); 112 | }.bind(this)); 113 | } 114 | }); 115 | 116 | .. jsx:: 117 | :hidesource: 118 | 119 | var columns = [ 120 | { 121 | name: 'ID', 122 | key: 'id', 123 | width: '20%' 124 | }, 125 | { 126 | name: 'Name', 127 | key: 'title', 128 | width: '80%' 129 | } 130 | ]; 131 | 132 | .. jsx:: 133 | 134 | React.renderComponent( 135 | , 140 | document.getElementById('example')); 141 | 142 | .. raw:: html 143 | 144 |
145 | -------------------------------------------------------------------------------- /docs/examples/index.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | :glob: 7 | 8 | selectable-rows 9 | selectable-cells 10 | locked-columns 11 | custom-column-formatters 12 | sortable-grid 13 | datagrid 14 | -------------------------------------------------------------------------------- /docs/examples/locked-columns.rst: -------------------------------------------------------------------------------- 1 | Locked columns 2 | ============== 3 | 4 | .. jsx:: 5 | :hidesource: 6 | 7 | var React = require('react') 8 | var ReactGrid = require('react-grid') 9 | 10 | var columns = [ 11 | { 12 | name: '№', 13 | width: '20%', 14 | locked: true, 15 | resizeable: true, 16 | key: 0 17 | }, 18 | { 19 | name: 'Name', 20 | width: '20%', 21 | locked: true, 22 | resizeable: true, 23 | key: 1 24 | }, 25 | { 26 | name: 'Surname', 27 | width: '40%', 28 | resizeable: true, 29 | key: 2 30 | }, 31 | { 32 | name: 'Surname 2', 33 | width: '40%', 34 | resizeable: true, 35 | key: 2 36 | } 37 | ]; 38 | 39 | .. jsx:: 40 | :hidesource: 41 | 42 | function rows(start, end) { 43 | var rows = []; 44 | for (var i = start; i < end; i++) { 45 | rows.push([i, 'Name ' + i, 'Surname ' + i]); 46 | } 47 | return rows; 48 | } 49 | 50 | React.renderComponent( 51 | , 56 | document.getElementById('example')); 57 | 58 | .. raw:: html 59 | 60 |
61 | -------------------------------------------------------------------------------- /docs/examples/selectable-cells.rst: -------------------------------------------------------------------------------- 1 | Selectable cells 2 | ================ 3 | 4 | .. jsx:: 5 | :hidesource: 6 | 7 | var React = require('react') 8 | var BaseGrid = require('react-grid') 9 | 10 | Grid component 11 | -------------- 12 | 13 | .. jsx:: 14 | 15 | var Grid = React.createClass({ 16 | 17 | render: function() { 18 | var cellRenderer = ( 19 | 24 | ); 25 | return this.transferPropsTo( 26 | 27 | ) 28 | }, 29 | 30 | getInitialState: function() { 31 | return {selected: null}; 32 | }, 33 | 34 | onSelect: function(selected) { 35 | var idx = selected.idx; 36 | var rowIdx = selected.rowIdx; 37 | if ( 38 | idx >= 0 39 | && rowIdx >= 0 40 | && idx < this.props.columns.length 41 | && rowIdx < this.props.length 42 | ) { 43 | this.setState({selected: selected}); 44 | } 45 | } 46 | }) 47 | 48 | Cell component 49 | -------------- 50 | 51 | .. jsx:: 52 | 53 | var Cell = React.createClass({ 54 | 55 | render: function() { 56 | var selected = this.isSelected(); 57 | return this.transferPropsTo( 58 | 65 | ) 66 | }, 67 | 68 | isSelected: function() { 69 | return ( 70 | this.props.selected 71 | && this.props.selected.rowIdx === this.props.rowIdx 72 | && this.props.selected.idx === this.props.idx 73 | ); 74 | }, 75 | 76 | onClick: function() { 77 | var rowIdx = this.props.rowIdx; 78 | var idx = this.props.idx; 79 | this.props.onClick({rowIdx: rowIdx, idx: idx}); 80 | }, 81 | 82 | onKeyDown: function(e) { 83 | var rowIdx = this.props.rowIdx; 84 | var idx = this.props.idx; 85 | if (e.key === 'ArrowUp') { 86 | e.stopPropagation(); 87 | e.preventDefault(); 88 | this.props.onSelect({idx: idx, rowIdx: rowIdx - 1}); 89 | } else if (e.key === 'ArrowDown') { 90 | e.stopPropagation(); 91 | e.preventDefault(); 92 | this.props.onSelect({idx: idx, rowIdx: rowIdx + 1}); 93 | } else if (e.key === 'ArrowLeft') { 94 | e.stopPropagation(); 95 | e.preventDefault(); 96 | this.props.onSelect({idx: idx - 1, rowIdx: rowIdx}); 97 | } else if (e.key === 'ArrowRight') { 98 | e.stopPropagation(); 99 | e.preventDefault(); 100 | this.props.onSelect({idx: idx + 1, rowIdx: rowIdx}); 101 | } 102 | }, 103 | 104 | setScrollLeft: function(scrollLeft) { 105 | this.refs.row.setScrollLeft(scrollLeft) 106 | }, 107 | 108 | componentDidMount: function() { 109 | this.checkFocus(); 110 | }, 111 | 112 | componentDidUpdate: function() { 113 | this.checkFocus(); 114 | }, 115 | 116 | checkFocus: function() { 117 | if (this.isSelected()) { 118 | this.getDOMNode().focus(); 119 | } 120 | } 121 | }) 122 | 123 | Example code 124 | ------------ 125 | 126 | .. jsx:: 127 | :hidesource: 128 | 129 | var columns = [ 130 | { 131 | key: 'id', 132 | name: 'ID', 133 | width: '20%' 134 | }, 135 | { 136 | key: 'title', 137 | name: 'Title' 138 | }, 139 | { 140 | key: 'count', 141 | name: 'Count', 142 | width: '20%' 143 | }, 144 | ] 145 | 146 | var rows = function(start, end) { 147 | var result = [] 148 | for (var i = start; i < end; i++) { 149 | result.push({ 150 | id: i, 151 | title: 'Title ' + i, 152 | count: i * 1000 153 | }); 154 | } 155 | return result; 156 | } 157 | 158 | React.renderComponent( 159 | , 160 | document.getElementById('example')) 161 | 162 | .. raw:: html 163 | 164 | 185 |
186 | 187 | -------------------------------------------------------------------------------- /docs/examples/selectable-rows.rst: -------------------------------------------------------------------------------- 1 | Selectable rows 2 | =============== 3 | 4 | .. jsx:: 5 | :hidesource: 6 | 7 | var React = require('react') 8 | var BaseGrid = require('react-grid') 9 | 10 | Grid component 11 | -------------- 12 | 13 | .. jsx:: 14 | 15 | var Grid = React.createClass({ 16 | 17 | render: function() { 18 | var rowRenderer = ( 19 | 24 | ); 25 | return this.transferPropsTo( 26 | 27 | ) 28 | }, 29 | 30 | getInitialState: function() { 31 | return {selected: null}; 32 | }, 33 | 34 | onSelect: function(idx) { 35 | if (idx >= 0 && idx < this.props.length) { 36 | this.setState({selected: idx}); 37 | } 38 | } 39 | }) 40 | 41 | Row component 42 | ------------- 43 | 44 | .. jsx:: 45 | 46 | var Row = React.createClass({ 47 | 48 | render: function() { 49 | var selected = this.isSelected(); 50 | return this.transferPropsTo( 51 | 58 | ) 59 | }, 60 | 61 | isSelected: function() { 62 | return this.props.selected === this.props.idx; 63 | }, 64 | 65 | onClick: function() { 66 | this.props.onClick(this.props.idx); 67 | }, 68 | 69 | onKeyDown: function(e) { 70 | if (e.key === 'ArrowUp') { 71 | e.stopPropagation(); 72 | e.preventDefault(); 73 | this.props.onSelect(this.props.idx - 1); 74 | } else if (e.key === 'ArrowDown') { 75 | e.stopPropagation(); 76 | e.preventDefault(); 77 | this.props.onSelect(this.props.idx + 1); 78 | } 79 | }, 80 | 81 | setScrollLeft: function(scrollLeft) { 82 | this.refs.row.setScrollLeft(scrollLeft) 83 | }, 84 | 85 | componentDidMount: function() { 86 | this.checkFocus(); 87 | }, 88 | 89 | componentDidUpdate: function() { 90 | this.checkFocus(); 91 | }, 92 | 93 | checkFocus: function() { 94 | if (this.isSelected()) { 95 | this.getDOMNode().focus(); 96 | } 97 | } 98 | }) 99 | 100 | Example code 101 | ------------ 102 | 103 | .. jsx:: 104 | :hidesource: 105 | 106 | var columns = [ 107 | { 108 | key: 'id', 109 | name: 'ID', 110 | width: '20%' 111 | }, 112 | { 113 | key: 'title', 114 | name: 'Title' 115 | }, 116 | { 117 | key: 'count', 118 | name: 'Count', 119 | width: '20%' 120 | }, 121 | ] 122 | 123 | var rows = function(start, end) { 124 | var result = [] 125 | for (var i = start; i < end; i++) { 126 | result.push({ 127 | id: i, 128 | title: 'Title ' + i, 129 | count: i * 1000 130 | }); 131 | } 132 | return result; 133 | } 134 | 135 | React.renderComponent( 136 | , 137 | document.getElementById('example')) 138 | 139 | .. raw:: html 140 | 141 | 151 |
152 | -------------------------------------------------------------------------------- /docs/examples/sortable-grid.rst: -------------------------------------------------------------------------------- 1 | Sortable grid 2 | ============= 3 | 4 | .. jsx:: 5 | :hidesource: 6 | 7 | var React = require('react') 8 | var ReactGrid = require('react-grid') 9 | 10 | .. jsx:: 11 | 12 | var SortableHeaderCell = React.createClass({ 13 | 14 | onClick: function() { 15 | this.props.column.sortBy( 16 | this.props.column, 17 | this.props.column.sorted); 18 | }, 19 | 20 | render: function() { 21 | var sorted = this.props.column.sorted; 22 | return ( 23 |
26 | {this.props.column.name} 27 | 28 | {sorted ? 29 | ' ' + (sorted === 'asc' ? '↓' : '↑') : 30 | ''} 31 | 32 |
33 | ); 34 | } 35 | }); 36 | 37 | function shallowCloneObject(o) { 38 | var r = {}; 39 | for (var k in o) { 40 | r[k] = o[k]; 41 | } 42 | return r; 43 | } 44 | 45 | var SortableGrid = React.createClass({ 46 | 47 | getInitialState: function() { 48 | return {sortDirection: null, sortColumn: null}; 49 | }, 50 | 51 | getDecoratedColumns: function(columns) { 52 | return this.props.columns.map(function(column) { 53 | column = shallowCloneObject(column); 54 | if (column.sortable) { 55 | column.headerRenderer = SortableHeaderCell; 56 | column.sortBy = this.sortBy; 57 | if (this.state.sortColumn === column.id) { 58 | column.sorted = this.state.sortDirection; 59 | } 60 | } 61 | return column 62 | }, this); 63 | }, 64 | 65 | sortBy: function(column, direction) { 66 | direction = direction === 'asc' ? 'desc' : 'asc' 67 | this.setState({sortDirection: direction, sortColumn: column.id}); 68 | }, 69 | 70 | rows: function(start, end) { 71 | return this.props.rows( 72 | start, end, 73 | this.props.length, 74 | this.state.sortColumn, 75 | this.state.sortDirection); 76 | }, 77 | 78 | render: function() { 79 | return this.transferPropsTo( 80 | 83 | ); 84 | } 85 | }); 86 | 87 | 88 | .. jsx:: 89 | :hidesource: 90 | 91 | var columns = [ 92 | { 93 | id: 'num', 94 | name: '№', 95 | width: '20%', 96 | key: 0, 97 | sortable: true 98 | }, 99 | { 100 | id: 'name', 101 | name: 'Name', 102 | width: '30%', 103 | key: 1, 104 | sortable: true 105 | }, 106 | { 107 | id: 'surname', 108 | name: 'Surname', 109 | width: '50%', 110 | key: 2 111 | } 112 | ]; 113 | 114 | function rows(start, end, length, sortColumn, sortDirection) { 115 | var rows = []; 116 | for (var i = start; i < end; i++) { 117 | var n = sortDirection === 'asc' ? i : length - i - 1; 118 | rows.push([n, 'Name ' + n, 'Surname ' + n]); 119 | } 120 | return rows; 121 | } 122 | 123 | React.renderComponent( 124 | , 129 | document.getElementById('example')); 130 | 131 | .. raw:: html 132 | 133 |
134 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | React Grid 2 | ========== 3 | 4 | React Grid provides data grid component for React_. 5 | 6 | Getting started 7 | --------------- 8 | 9 | React Grid is available through npm:: 10 | 11 | npm instal react 12 | npm instal git+https://github.com/prometheusresearch/react-grid 13 | 14 | Then you can require ``react`` and ``react-grid`` in your code: 15 | 16 | .. jsx:: 17 | 18 | var React = require('react') 19 | var ReactGrid = require('react-grid') 20 | 21 | ``ReactGrid`` component requires at least ``columns`` and ``rows`` properties to 22 | be specified. 23 | 24 | The ``columns`` is a column specification, it provides information to grid on 25 | how to extract data for each of the column and how column should be represented 26 | and its features: 27 | 28 | .. jsx:: 29 | 30 | var columns = [ 31 | { 32 | key: 'id', 33 | name: 'ID', 34 | width: '20%', 35 | resizeable: true 36 | }, 37 | { 38 | key: 'title', 39 | name: 'Title' 40 | }, 41 | { 42 | key: 'count', 43 | name: 'Count', 44 | width: '20%' 45 | }, 46 | ] 47 | 48 | The ``rows`` property can be an array or a function which returns a slice of 49 | data given the specified range. If we specify ``rows`` as a function, we should 50 | also provide ``length`` property: 51 | 52 | .. jsx:: 53 | 54 | var rows = function(start, end) { 55 | var result = [] 56 | for (var i = start; i < end; i++) { 57 | result.push({ 58 | id: i, 59 | title: 'Title ' + i, 60 | count: i * 1000 61 | }); 62 | } 63 | return result; 64 | } 65 | 66 | Now simply invoke ``React.renderComponent(..)``: 67 | 68 | .. jsx:: 69 | 70 | React.renderComponent( 71 | , 72 | document.getElementById('example')) 73 | 74 | The code above will result in a grid: 75 | 76 | .. raw:: html 77 | 78 |
79 | 80 | .. toctree:: 81 | :maxdepth: 3 82 | :hidden: 83 | 84 | self 85 | styling 86 | examples/index 87 | api 88 | 89 | .. _React: http://facebook.github.io/react 90 | -------------------------------------------------------------------------------- /docs/styling.rst: -------------------------------------------------------------------------------- 1 | Styling 2 | ======= 3 | 4 | ``.react-grid-Grid`` 5 | Entire Grid. 6 | 7 | Header 8 | ------ 9 | 10 | ``.react-grid-Header`` 11 | Header. 12 | 13 | ``.react-grid-HeaderRow`` 14 | Header row. 15 | 16 | ``.react-grid-HeaderCell`` 17 | Header cell. 18 | 19 | ``.react-grid-HeaderCell--locked`` 20 | Header cell when locked. 21 | 22 | ``.react-grid-HeaderCell--resizing`` 23 | Header cell when resizing. 24 | 25 | ``.react-grid-HeaderCell__resizeHandle`` 26 | Resize handle of the header cell. 27 | 28 | Viewport 29 | -------- 30 | 31 | ``.react-grid-Viewport`` 32 | Viewport. 33 | 34 | ``.react-grid-Canvas`` 35 | Canvas. 36 | 37 | ``.react-grid-Row`` 38 | Row. 39 | 40 | ``.react-grid-Row--even`` 41 | Row when even. 42 | 43 | ``.react-grid-Row--odd`` 44 | Row when odd. 45 | 46 | ``.react-grid-Cell`` 47 | Cell. 48 | 49 | ``.react-grid-Cell--locked`` 50 | Cell when locked. 51 | -------------------------------------------------------------------------------- /lib/Canvas.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | * @copyright Prometheus Research, LLC 2014 4 | */ 5 | "use strict"; 6 | 7 | var React = require('react/addons'); 8 | var cx = React.addons.classSet; 9 | var PropTypes = React.PropTypes; 10 | var cloneWithProps = React.addons.cloneWithProps; 11 | var shallowEqual = require('./shallowEqual'); 12 | var emptyFunction = require('./emptyFunction'); 13 | var ScrollShim = require('./ScrollShim'); 14 | var Row = require('./Row'); 15 | 16 | var Canvas = React.createClass({ 17 | mixins: [ScrollShim], 18 | 19 | propTypes: { 20 | cellRenderer: PropTypes.component, 21 | rowRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.component]), 22 | rowHeight: PropTypes.number.isRequired, 23 | displayStart: PropTypes.number.isRequired, 24 | displayEnd: PropTypes.number.isRequired, 25 | length: PropTypes.number.isRequired, 26 | rows: PropTypes.oneOfType([ 27 | PropTypes.func.isRequired, 28 | PropTypes.array.isRequired 29 | ]), 30 | onRows: PropTypes.func 31 | }, 32 | 33 | render() { 34 | var displayStart = this.state.displayStart; 35 | var displayEnd = this.state.displayEnd; 36 | var rowHeight = this.props.rowHeight; 37 | var length = this.props.length; 38 | 39 | var rows = this 40 | .getRows(displayStart, displayEnd) 41 | .map((row, idx) => this.renderRow({ 42 | key: displayStart + idx, 43 | ref: idx, 44 | idx: displayStart + idx, 45 | row: row, 46 | height: rowHeight, 47 | columns: this.props.columns, 48 | cellRenderer: this.props.cellRenderer 49 | })); 50 | 51 | this._currentRowsLength = rows.length; 52 | 53 | if (displayStart > 0) { 54 | rows.unshift(this.renderPlaceholder('top', displayStart * rowHeight)); 55 | } 56 | 57 | if (length - displayEnd > 0) { 58 | rows.push( 59 | this.renderPlaceholder('bottom', (length - displayEnd) * rowHeight)); 60 | } 61 | 62 | var style = { 63 | position: 'absolute', 64 | top: 0, 65 | left: 0, 66 | overflowX: 'auto', 67 | overflowY: 'scroll', 68 | width: this.props.totalWidth, 69 | height: this.props.height, 70 | transform: 'translate3d(0, 0, 0)' 71 | }; 72 | 73 | return ( 74 |
78 |
79 | {rows} 80 |
81 |
82 | ); 83 | }, 84 | 85 | renderRow(props) { 86 | if (React.isValidComponent(this.props.rowRenderer)) { 87 | return cloneWithProps(this.props.rowRenderer, props); 88 | } else { 89 | return this.props.rowRenderer(props); 90 | } 91 | }, 92 | 93 | renderPlaceholder(key, height) { 94 | return ( 95 |
96 | {this.props.columns.map( 97 | (column, idx) =>
)} 98 |
99 | ); 100 | }, 101 | 102 | getDefaultProps() { 103 | return { 104 | rowRenderer: Row, 105 | onRows: emptyFunction 106 | }; 107 | }, 108 | 109 | getInitialState() { 110 | return { 111 | shouldUpdate: true, 112 | displayStart: this.props.displayStart, 113 | displayEnd: this.props.displayEnd 114 | }; 115 | }, 116 | 117 | componentWillMount() { 118 | this._currentRowsLength = undefined; 119 | this._currentRowsRange = undefined; 120 | this._scroll = undefined; 121 | }, 122 | 123 | componentDidMount() { 124 | this.onRows(); 125 | }, 126 | 127 | componentDidUpdate() { 128 | if (this._scroll !== undefined) { 129 | this.setScrollLeft(this._scroll); 130 | } 131 | this.onRows(); 132 | }, 133 | 134 | componentWillUnmount() { 135 | this._currentRowsLength = undefined; 136 | this._currentRowsRange = undefined; 137 | this._scroll = undefined; 138 | }, 139 | 140 | componentWillReceiveProps(nextProps) { 141 | var shouldUpdate = !(nextProps.visibleStart > this.state.displayStart 142 | && nextProps.visibleEnd < this.state.displayEnd) 143 | || nextProps.length !== this.props.length 144 | || nextProps.rowHeight !== this.props.rowHeight 145 | || nextProps.columns !== this.props.columns 146 | || nextProps.width !== this.props.width 147 | || !shallowEqual(nextProps.style, this.props.style); 148 | 149 | if (shouldUpdate) { 150 | this.setState({ 151 | shouldUpdate: true, 152 | displayStart: nextProps.displayStart, 153 | displayEnd: nextProps.displayEnd 154 | }); 155 | } else { 156 | this.setState({shouldUpdate: false}); 157 | } 158 | }, 159 | 160 | shouldComponentUpdate(nextProps, nextState) { 161 | return nextState.shouldUpdate; 162 | }, 163 | 164 | onRows() { 165 | if (this._currentRowsRange !== undefined) { 166 | this.props.onRows(this._currentRowsRange); 167 | this._currentRowsRange = undefined; 168 | } 169 | }, 170 | 171 | getRows(displayStart, displayEnd) { 172 | this._currentRowsRange = {start: displayStart, end: displayEnd}; 173 | if (Array.isArray(this.props.rows)) { 174 | return this.props.rows.slice(displayStart, displayEnd); 175 | } else { 176 | return this.props.rows(displayStart, displayEnd); 177 | } 178 | }, 179 | 180 | setScrollLeft(scrollLeft) { 181 | if (this._currentRowsLength !== undefined) { 182 | for (var i = 0, len = this._currentRowsLength; i < len; i++) { 183 | this.refs[i].setScrollLeft(scrollLeft); 184 | } 185 | } 186 | }, 187 | 188 | getScroll() { 189 | var {scrollTop, scrollLeft} = this.getDOMNode(); 190 | return {scrollTop, scrollLeft}; 191 | }, 192 | 193 | onScroll(e) { 194 | this.appendScrollShim(); 195 | var {scrollTop, scrollLeft} = e.target; 196 | var scroll = {scrollTop, scrollLeft}; 197 | this._scroll = scroll; 198 | this.props.onScroll(scroll); 199 | } 200 | }); 201 | 202 | 203 | module.exports = Canvas; 204 | -------------------------------------------------------------------------------- /lib/Cell.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | * @copyright Prometheus Research, LLC 2014 4 | */ 5 | 'use strict'; 6 | 7 | var React = require('react/addons'); 8 | var cx = React.addons.classSet; 9 | var cloneWithProps = React.addons.cloneWithProps; 10 | 11 | var Cell = React.createClass({ 12 | 13 | render() { 14 | var style = this.getStyle(); 15 | var className = cx( 16 | 'react-grid-Cell', 17 | this.props.className, 18 | this.props.column.locked ? 'react-grid-Cell--locked' : null 19 | ); 20 | return this.transferPropsTo( 21 |
22 | 26 |
27 | ); 28 | }, 29 | 30 | renderCellContent(props) { 31 | var content = React.isValidComponent(this.props.formatter) ? 32 | cloneWithProps(this.props.formatter, props) : 33 | this.props.formatter(props); 34 | return ( 35 |
36 | {content} 37 |
38 | ); 39 | }, 40 | 41 | getDefaultProps() { 42 | return { 43 | formatter: simpleCellFormatter 44 | }; 45 | }, 46 | 47 | getStyle() { 48 | var style = { 49 | position: 'absolute', 50 | width: this.props.column.width, 51 | height: this.props.height, 52 | left: this.props.column.left 53 | }; 54 | return style; 55 | }, 56 | 57 | setScrollLeft(scrollLeft) { 58 | if (this.isMounted()) { 59 | var node = this.getDOMNode(); 60 | var transform = `translate3d(${scrollLeft}px, 0px, 0px)`; 61 | node.style.webkitTransform = transform; 62 | node.style.transform = transform; 63 | } 64 | } 65 | }); 66 | 67 | function simpleCellFormatter(props) { 68 | return props.value; 69 | } 70 | 71 | module.exports = Cell; 72 | -------------------------------------------------------------------------------- /lib/ColumnMetrics.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | * @copyright Prometheus Research, LLC 2014 4 | */ 5 | "use strict"; 6 | 7 | var {PropTypes, isValidComponent} = require('react'); 8 | var shallowCloneObject = require('./shallowCloneObject'); 9 | var DOMMetrics = require('./DOMMetrics'); 10 | 11 | /** 12 | * Update column metrics calculation. 13 | * 14 | * @param {ColumnMetrics} metrics 15 | */ 16 | function calculate(metrics) { 17 | var width = 0; 18 | var unallocatedWidth = metrics.totalWidth; 19 | 20 | var deferredColumns = []; 21 | var columns = metrics.columns.map(shallowCloneObject); 22 | 23 | var i, len, column; 24 | 25 | // compute width for columns which specify width 26 | for (i = 0, len = columns.length; i < len; i++) { 27 | column = columns[i]; 28 | 29 | if (column.width) { 30 | if (/^([0-9]+)%$/.exec(column.width)) { 31 | column.width = Math.floor( 32 | parseInt(column.width, 10) / 100 * metrics.totalWidth); 33 | } 34 | unallocatedWidth -= column.width; 35 | width += column.width; 36 | } else { 37 | deferredColumns.push(column); 38 | } 39 | 40 | } 41 | 42 | // compute width for columns which doesn't specify width 43 | for (i = 0, len = deferredColumns.length; i < len; i++) { 44 | column = deferredColumns[i]; 45 | 46 | if (unallocatedWidth <= 0) { 47 | column.width = metrics.minColumnWidth; 48 | } else { 49 | column.width = Math.floor(unallocatedWidth / deferredColumns.length); 50 | } 51 | width += column.width; 52 | } 53 | 54 | // compute left offset 55 | var left = 0; 56 | for (i = 0, len = columns.length; i < len; i++) { 57 | column = columns[i]; 58 | column.left = left; 59 | left += column.width; 60 | } 61 | 62 | return { 63 | columns, 64 | width, 65 | totalWidth: metrics.totalWidth, 66 | minColumnWidth: metrics.minColumnWidth 67 | }; 68 | } 69 | 70 | /** 71 | * Update column metrics calculation by resizing a column. 72 | * 73 | * @param {ColumnMetrics} metrics 74 | * @param {Column} column 75 | * @param {number} width 76 | */ 77 | function resizeColumn(metrics, index, width) { 78 | var column = metrics.columns[index]; 79 | metrics = shallowCloneObject(metrics); 80 | metrics.columns = metrics.columns.slice(0); 81 | 82 | var updatedColumn = shallowCloneObject(column); 83 | updatedColumn.width = Math.max(width, metrics.minColumnWidth); 84 | 85 | metrics.columns.splice(index, 1, updatedColumn); 86 | 87 | return calculate(metrics); 88 | } 89 | 90 | var Mixin = { 91 | mixins: [DOMMetrics.MetricsMixin], 92 | 93 | propTypes: { 94 | columns: PropTypes.array, 95 | minColumnWidth: PropTypes.number, 96 | columnEquality: PropTypes.func 97 | }, 98 | 99 | DOMMetrics: { 100 | gridWidth() { 101 | return this.getDOMNode().offsetWidth - 2; 102 | } 103 | }, 104 | 105 | getDefaultProps() { 106 | return { 107 | minColumnWidth: 80, 108 | columnEquality: sameColumn 109 | }; 110 | }, 111 | 112 | getInitialState() { 113 | return this.getColumnMetrics(this.props, true); 114 | }, 115 | 116 | componentWillReceiveProps(nextProps) { 117 | if (nextProps.columns) { 118 | if (!sameColumns(this.props.columns, nextProps.columns, this.props.columnEquality)) { 119 | this.setState(this.getColumnMetrics(nextProps)); 120 | } else { 121 | var index = {}; 122 | this.state.columns.columns.forEach((c) => { 123 | index[c.key] = {width: c.width, left: c.left}; 124 | }); 125 | var nextColumns = merge(this.state.columns, { 126 | columns: nextProps.columns.map((c) => merge(c, index[c.key])) 127 | }); 128 | this.setState({columns: nextColumns}); 129 | } 130 | } 131 | }, 132 | 133 | getColumnMetrics(props, initial) { 134 | var totalWidth = initial ? null : this.DOMMetrics.gridWidth(); 135 | return { 136 | columns: calculate({ 137 | columns: props.columns, 138 | width: null, 139 | totalWidth, 140 | minColumnWidth: props.minColumnWidth 141 | }), 142 | gridWidth: totalWidth 143 | }; 144 | }, 145 | 146 | metricsUpdated() { 147 | this.setState(this.getColumnMetrics(this.props)); 148 | }, 149 | 150 | onColumnResize(index, width) { 151 | var columns = resizeColumn(this.state.columns, index, width); 152 | this.setState({columns}); 153 | } 154 | }; 155 | 156 | function sameColumns(prevColumns, nextColumns, sameColumn) { 157 | var i, len, column; 158 | var prevColumnsByKey = {}; 159 | var nextColumnsByKey = {}; 160 | 161 | for (i = 0, len = prevColumns.length; i < len; i++) { 162 | column = prevColumns[i]; 163 | prevColumnsByKey[column.key] = column; 164 | } 165 | 166 | for (i = 0, len = nextColumns.length; i < len; i++) { 167 | column = nextColumns[i]; 168 | nextColumnsByKey[column.key] = column; 169 | var prevColumn = prevColumnsByKey[column.key]; 170 | if (prevColumn === undefined || !sameColumn(prevColumn, column)) { 171 | return false; 172 | } 173 | } 174 | 175 | for (i = 0, len = prevColumns.length; i < len; i++) { 176 | column = prevColumns[i]; 177 | var nextColumn = nextColumnsByKey[column.key]; 178 | if (nextColumn === undefined) { 179 | return false; 180 | } 181 | } 182 | 183 | return true; 184 | } 185 | 186 | function merge(a, b) { 187 | var k; 188 | var r = {}; 189 | 190 | for (var k in a) { 191 | if (a.hasOwnProperty(k)) { 192 | r[k] = a[k]; 193 | } 194 | } 195 | for (var k in b) { 196 | if (b.hasOwnProperty(k)) { 197 | r[k] = b[k]; 198 | } 199 | } 200 | 201 | return r; 202 | } 203 | 204 | function sameColumn(a, b) { 205 | var k; 206 | 207 | for (k in a) { 208 | if (a.hasOwnProperty(k)) { 209 | if (typeof a[k] === 'function' && typeof b[k] === 'function') { 210 | continue; 211 | } 212 | if (!b.hasOwnProperty(k) || a[k] !== b[k]) { 213 | return false; 214 | } 215 | } 216 | } 217 | 218 | for (k in b) { 219 | if (b.hasOwnProperty(k) && !a.hasOwnProperty(k)) { 220 | return false; 221 | } 222 | } 223 | 224 | return true; 225 | } 226 | 227 | module.exports = {Mixin, calculate, resizeColumn, sameColumns, sameColumn}; 228 | -------------------------------------------------------------------------------- /lib/DOMMetrics.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | * @copyright Prometheus Research, LLC 2014 4 | */ 5 | 'use strict'; 6 | 7 | var React = require('react'); 8 | var emptyFunction = require('./emptyFunction'); 9 | var shallowCloneObject = require('./shallowCloneObject'); 10 | var invariant = require('./invariant'); 11 | 12 | var contextTypes = { 13 | metricsComputator: React.PropTypes.object 14 | }; 15 | 16 | var MetricsComputatorMixin = { 17 | 18 | childContextTypes: contextTypes, 19 | 20 | getChildContext() { 21 | return {metricsComputator: this}; 22 | }, 23 | 24 | getMetricImpl(name) { 25 | return this._DOMMetrics.metrics[name].value; 26 | }, 27 | 28 | registerMetricsImpl(component, metrics) { 29 | var getters = {}; 30 | var s = this._DOMMetrics; 31 | 32 | for (var name in metrics) { 33 | invariant( 34 | s.metrics[name] === undefined, 35 | 'DOM metric ' + name + ' is already defined' 36 | ); 37 | s.metrics[name] = {component, computator: metrics[name].bind(component)}; 38 | getters[name] = this.getMetricImpl.bind(null, name); 39 | } 40 | 41 | if (s.components.indexOf(component) === -1) { 42 | s.components.push(component); 43 | } 44 | 45 | return getters; 46 | }, 47 | 48 | unregisterMetricsFor(component) { 49 | var s = this._DOMMetrics; 50 | var idx = s.components.indexOf(component); 51 | 52 | if (idx > -1) { 53 | s.components.splice(idx, 1); 54 | 55 | var name; 56 | var metricsToDelete = {}; 57 | 58 | for (name in s.metrics) { 59 | if (s.metrics[name].component === component) { 60 | metricsToDelete[name] = true; 61 | } 62 | } 63 | 64 | for (name in metricsToDelete) { 65 | delete s.metrics[name]; 66 | } 67 | } 68 | }, 69 | 70 | updateMetrics() { 71 | var s = this._DOMMetrics; 72 | 73 | var needUpdate = false; 74 | 75 | for (var name in s.metrics) { 76 | var newMetric = s.metrics[name].computator(); 77 | if (newMetric !== s.metrics[name].value) { 78 | needUpdate = true; 79 | } 80 | s.metrics[name].value = newMetric; 81 | } 82 | 83 | if (needUpdate) { 84 | for (var i = 0, len = s.components.length; i < len; i++) { 85 | if (s.components[i].metricsUpdated) { 86 | s.components[i].metricsUpdated(); 87 | } 88 | } 89 | } 90 | }, 91 | 92 | componentWillMount() { 93 | this._DOMMetrics = { 94 | metrics: {}, 95 | components: [] 96 | }; 97 | }, 98 | 99 | componentDidMount() { 100 | window.addEventListener('resize', this.updateMetrics); 101 | this.updateMetrics(); 102 | }, 103 | 104 | componentWillUnmount() { 105 | window.removeEventListener('resize', this.updateMetrics); 106 | } 107 | 108 | }; 109 | 110 | var MetricsMixin = { 111 | 112 | contextTypes: contextTypes, 113 | 114 | componentWillMount() { 115 | if (this.DOMMetrics) { 116 | this._DOMMetricsDefs = shallowCloneObject(this.DOMMetrics); 117 | 118 | this.DOMMetrics = {}; 119 | for (var name in this._DOMMetricsDefs) { 120 | this.DOMMetrics[name] = emptyFunction; 121 | } 122 | } 123 | }, 124 | 125 | componentDidMount() { 126 | if (this.DOMMetrics) { 127 | this.DOMMetrics = this.registerMetrics(this._DOMMetricsDefs); 128 | } 129 | }, 130 | 131 | componentWillUnmount() { 132 | if (!this.registerMetricsImpl) { 133 | return this.context.metricsComputator.unregisterMetricsFor(this); 134 | } 135 | if (this.hasOwnProperty('DOMMetrics')) { 136 | delete this.DOMMetrics; 137 | } 138 | }, 139 | 140 | registerMetrics(metrics) { 141 | if (this.registerMetricsImpl) { 142 | return this.registerMetricsImpl(this, metrics); 143 | } else { 144 | return this.context.metricsComputator.registerMetricsImpl(this, metrics); 145 | } 146 | }, 147 | 148 | getMetric(name) { 149 | if (this.getMetricImpl) { 150 | return this.getMetricImpl(name); 151 | } else { 152 | return this.context.metricsComputator.getMetricImpl(name); 153 | } 154 | } 155 | }; 156 | 157 | module.exports = { 158 | MetricsComputatorMixin, 159 | MetricsMixin 160 | }; 161 | -------------------------------------------------------------------------------- /lib/Draggable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | * @copyright Prometheus Research, LLC 2014 4 | */ 5 | 'use strict'; 6 | 7 | var React = require('react'); 8 | var PropTypes = React.PropTypes; 9 | var emptyFunction = require('./emptyFunction'); 10 | 11 | var Draggable = React.createClass({ 12 | 13 | propTypes: { 14 | onDragStart: PropTypes.func, 15 | onDragEnd: PropTypes.func, 16 | onDrag: PropTypes.func, 17 | component: PropTypes.oneOfType([PropTypes.func, PropTypes.constructor]) 18 | }, 19 | 20 | render() { 21 | var component = this.props.component; 22 | return this.transferPropsTo( 23 | 24 | ); 25 | }, 26 | 27 | getDefaultProps() { 28 | return { 29 | component: React.DOM.div, 30 | onDragStart: emptyFunction.thatReturnsTrue, 31 | onDragEnd: emptyFunction, 32 | onDrag: emptyFunction 33 | }; 34 | }, 35 | 36 | getInitialState() { 37 | return { 38 | drag: null 39 | }; 40 | }, 41 | 42 | onMouseDown(e) { 43 | var drag = this.props.onDragStart(e); 44 | 45 | if (drag === null && e.button !== 0) { 46 | return; 47 | } 48 | 49 | window.addEventListener('mouseup', this.onMouseUp); 50 | window.addEventListener('mousemove', this.onMouseMove); 51 | 52 | this.setState({drag}); 53 | }, 54 | 55 | onMouseMove(e) { 56 | if (this.state.drag === null) { 57 | return; 58 | } 59 | 60 | if (e.stopPropagation) { 61 | e.stopPropagation(); 62 | } 63 | 64 | if (e.preventDefault) { 65 | e.preventDefault(); 66 | } 67 | 68 | this.props.onDrag(e); 69 | }, 70 | 71 | onMouseUp(e) { 72 | this.cleanUp(); 73 | this.props.onDragEnd(e, this.state.drag); 74 | this.setState({drag: null}); 75 | }, 76 | 77 | componentWillUnmount() { 78 | this.cleanUp(); 79 | }, 80 | 81 | cleanUp() { 82 | window.removeEventListener('mouseup', this.onMouseUp); 83 | window.removeEventListener('mousemove', this.onMouseMove); 84 | } 85 | }); 86 | 87 | module.exports = Draggable; 88 | -------------------------------------------------------------------------------- /lib/Grid.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | * @copyright Prometheus Research, LLC 2014 4 | */ 5 | "use strict"; 6 | 7 | var React = require('react'); 8 | var PropTypes = React.PropTypes; 9 | var Header = require('./Header'); 10 | var Viewport = require('./Viewport'); 11 | var ColumnMetrics = require('./ColumnMetrics'); 12 | var DOMMetrics = require('./DOMMetrics'); 13 | 14 | var GridScrollMixin = { 15 | 16 | componentDidMount() { 17 | this._scrollLeft = this.refs.viewport.getScroll().scrollLeft; 18 | this._onScroll(); 19 | }, 20 | 21 | componentDidUpdate() { 22 | this._onScroll(); 23 | }, 24 | 25 | componentWillMount() { 26 | this._scrollLeft = undefined; 27 | }, 28 | 29 | componentWillUnmount() { 30 | this._scrollLeft = undefined; 31 | }, 32 | 33 | onScroll({scrollLeft}) { 34 | if (this._scrollLeft !== scrollLeft) { 35 | this._scrollLeft = scrollLeft; 36 | this._onScroll(); 37 | } 38 | }, 39 | 40 | _onScroll() { 41 | if (this._scrollLeft !== undefined) { 42 | this.refs.header.setScrollLeft(this._scrollLeft); 43 | this.refs.viewport.setScrollLeft(this._scrollLeft); 44 | } 45 | } 46 | }; 47 | 48 | var Grid = React.createClass({ 49 | mixins: [ 50 | GridScrollMixin, 51 | ColumnMetrics.Mixin, 52 | DOMMetrics.MetricsComputatorMixin 53 | ], 54 | 55 | propTypes: { 56 | rows: PropTypes.oneOfType([PropTypes.array, PropTypes.func]).isRequired, 57 | columns: PropTypes.array.isRequired 58 | }, 59 | 60 | style: { 61 | overflow: 'hidden', 62 | outline: 0 63 | }, 64 | 65 | render() { 66 | return this.transferPropsTo( 67 |
68 |
75 | 88 |
89 | ); 90 | }, 91 | 92 | getDefaultProps() { 93 | return { 94 | rowHeight: 35 95 | }; 96 | }, 97 | }); 98 | 99 | module.exports = Grid; 100 | -------------------------------------------------------------------------------- /lib/Header.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | * @copyright Prometheus Research, LLC 2014 4 | */ 5 | "use strict"; 6 | 7 | var React = require('react/addons'); 8 | var cx = React.addons.classSet; 9 | var shallowCloneObject = require('./shallowCloneObject'); 10 | var ColumnMetrics = require('./ColumnMetrics'); 11 | var HeaderRow = require('./HeaderRow'); 12 | 13 | var Header = React.createClass({ 14 | 15 | propTypes: { 16 | columns: React.PropTypes.object.isRequired, 17 | totalWidth: React.PropTypes.number, 18 | height: React.PropTypes.number.isRequired 19 | }, 20 | 21 | render() { 22 | var state = this.state.resizing || this.props; 23 | 24 | var headerRowStyle = { 25 | position: 'absolute', 26 | top: 0, 27 | left: 0, 28 | width: this.props.totalWidth 29 | }; 30 | 31 | var className = cx({ 32 | 'react-grid-Header': true, 33 | 'react-grid-Header--resizing': !!this.state.resizing 34 | }); 35 | 36 | return this.transferPropsTo( 37 |
38 | 48 |
49 | ); 50 | }, 51 | 52 | getInitialState() { 53 | return {resizing: null}; 54 | }, 55 | 56 | componentWillReceiveProps() { 57 | this.setState({resizing: null}); 58 | }, 59 | 60 | onColumnResize(column, width) { 61 | var state = this.state.resizing || this.props; 62 | 63 | var pos = this.getColumnPosition(column); 64 | 65 | if (pos !== null) { 66 | var resizing = { 67 | columns: shallowCloneObject(state.columns) 68 | }; 69 | resizing.columns = ColumnMetrics.resizeColumn( 70 | resizing.columns, pos, width); 71 | 72 | // we don't want to influence scrollLeft while resizing 73 | if (resizing.columns.width < state.columns.width) { 74 | resizing.columns.width = state.columns.width; 75 | } 76 | 77 | resizing.column = resizing.columns.columns[pos]; 78 | this.setState({resizing}); 79 | } 80 | }, 81 | 82 | getColumnPosition(column) { 83 | var state = this.state.resizing || this.props; 84 | var pos = state.columns.columns.indexOf(column); 85 | return pos === -1 ? null : pos; 86 | }, 87 | 88 | onColumnResizeEnd(column, width) { 89 | var pos = this.getColumnPosition(column); 90 | if (pos !== null && this.props.onColumnResize) { 91 | this.props.onColumnResize(pos, width || column.width); 92 | } 93 | }, 94 | 95 | setScrollLeft(scrollLeft) { 96 | var node = this.refs.row.getDOMNode(); 97 | node.scrollLeft = scrollLeft; 98 | this.refs.row.setScrollLeft(scrollLeft); 99 | }, 100 | 101 | getStyle() { 102 | return { 103 | position: 'relative', 104 | height: this.props.height 105 | }; 106 | } 107 | }); 108 | 109 | 110 | module.exports = Header; 111 | -------------------------------------------------------------------------------- /lib/HeaderCell.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | * @copyright Prometheus Research, LLC 2014 4 | */ 5 | "use strict"; 6 | 7 | var React = require('react/addons'); 8 | var cx = React.addons.classSet; 9 | var Draggable = require('./Draggable'); 10 | 11 | var ResizeHandle = React.createClass({ 12 | 13 | style: { 14 | position: 'absolute', 15 | top: 0, 16 | right: 0, 17 | width: 6, 18 | height: '100%' 19 | }, 20 | 21 | render() { 22 | return this.transferPropsTo( 23 | 27 | ); 28 | } 29 | }); 30 | 31 | var HeaderCell = React.createClass({ 32 | 33 | propTypes: { 34 | renderer: React.PropTypes.func, 35 | column: React.PropTypes.object.isRequired, 36 | onResize: React.PropTypes.func 37 | }, 38 | 39 | render() { 40 | var className = cx({ 41 | 'react-grid-HeaderCell': true, 42 | 'react-grid-HeaderCell--resizing': this.state.resizing, 43 | 'react-grid-HeaderCell--locked': this.props.column.locked 44 | }); 45 | className = cx(className, this.props.className); 46 | return ( 47 |
48 | 49 | {this.props.column.resizeable ? 50 | : 55 | null} 56 |
57 | ); 58 | }, 59 | 60 | getDefaultProps() { 61 | return { 62 | renderer: simpleCellRenderer 63 | }; 64 | }, 65 | 66 | getInitialState() { 67 | return {resizing: false}; 68 | }, 69 | 70 | setScrollLeft(scrollLeft) { 71 | var node = this.getDOMNode(); 72 | node.style.webkitTransform = `translate3d(${scrollLeft}px, 0px, 0px)`; 73 | node.style.transform = `translate3d(${scrollLeft}px, 0px, 0px)`; 74 | }, 75 | 76 | getStyle() { 77 | return { 78 | width: this.props.column.width, 79 | left: this.props.column.left, 80 | display: 'inline-block', 81 | position: 'absolute', 82 | overflow: 'hidden', 83 | height: this.props.height, 84 | margin: 0, 85 | textOverflow: 'ellipsis', 86 | whiteSpace: 'nowrap' 87 | }; 88 | }, 89 | 90 | onDragStart() { 91 | this.setState({resizing: true}); 92 | }, 93 | 94 | onDrag(e) { 95 | var width = this.getWidthFromMouseEvent(e); 96 | if (width > 0 && this.props.onResize) { 97 | this.props.onResize(this.props.column, width); 98 | } 99 | }, 100 | 101 | onDragEnd(e) { 102 | var width = this.getWidthFromMouseEvent(e); 103 | this.props.onResizeEnd(this.props.column, width); 104 | this.setState({resizing: false}); 105 | }, 106 | 107 | getWidthFromMouseEvent(e) { 108 | var right = e.pageX; 109 | var left = this.getDOMNode().getBoundingClientRect().left; 110 | return right - left; 111 | } 112 | }); 113 | 114 | function simpleCellRenderer(props) { 115 | return ( 116 |
117 | {props.column.name} 118 |
119 | ); 120 | } 121 | 122 | module.exports = HeaderCell; 123 | -------------------------------------------------------------------------------- /lib/HeaderRow.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | * @copyright Prometheus Research, LLC 2014 4 | */ 5 | "use strict"; 6 | 7 | var React = require('react/addons'); 8 | var PropTypes = React.PropTypes; 9 | var shallowEqual = require('./shallowEqual'); 10 | var HeaderCell = require('./HeaderCell'); 11 | var getScrollbarSize = require('./getScrollbarSize'); 12 | 13 | var HeaderRow = React.createClass({ 14 | 15 | propTypes: { 16 | width: PropTypes.number, 17 | height: PropTypes.number.isRequired, 18 | columns: PropTypes.array.isRequired, 19 | onColumnResize: PropTypes.func 20 | }, 21 | 22 | render() { 23 | var cellsStyle = { 24 | width: this.props.width ? (this.props.width + getScrollbarSize()) : '100%', 25 | height: this.props.height, 26 | whiteSpace: 'nowrap', 27 | overflowX: 'hidden', 28 | overflowY: 'hidden' 29 | }; 30 | 31 | return this.transferPropsTo( 32 |
33 |
34 | 35 |
36 |
37 | ); 38 | }, 39 | 40 | renderCells() { 41 | var cells = []; 42 | var lockedCells = []; 43 | 44 | for (var i = 0, len = this.props.columns.length; i < len; i++) { 45 | var column = this.props.columns[i]; 46 | var cell = ( 47 | 57 | ); 58 | if (column.locked) { 59 | lockedCells.push(cell); 60 | } else { 61 | cells.push(cell); 62 | } 63 | } 64 | 65 | return cells.concat(lockedCells); 66 | }, 67 | 68 | setScrollLeft(scrollLeft) { 69 | for (var i = 0, len = this.props.columns.length; i < len; i++) { 70 | if (this.props.columns[i].locked) { 71 | this.refs[i].setScrollLeft(scrollLeft); 72 | } 73 | } 74 | }, 75 | 76 | shouldComponentUpdate(nextProps) { 77 | return ( 78 | nextProps.width !== this.props.width 79 | || nextProps.height !== this.props.height 80 | || nextProps.columns !== this.props.columns 81 | || !shallowEqual(nextProps.style, this.props.style) 82 | ); 83 | }, 84 | 85 | getStyle() { 86 | return { 87 | overflow: 'hidden', 88 | width: '100%', 89 | height: this.props.height, 90 | position: 'absolute' 91 | }; 92 | } 93 | 94 | }); 95 | 96 | module.exports = HeaderRow; 97 | -------------------------------------------------------------------------------- /lib/Row.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | * @copyright Prometheus Research, LLC 2014 4 | */ 5 | 'use strict'; 6 | 7 | var React = require('react/addons'); 8 | var cx = React.addons.classSet; 9 | var Cell = require('./Cell'); 10 | var cloneWithProps = React.addons.cloneWithProps; 11 | 12 | var Row = React.createClass({ 13 | 14 | render() { 15 | var className = cx( 16 | 'react-grid-Row', 17 | `react-grid-Row--${this.props.idx % 2 === 0 ? 'even' : 'odd'}` 18 | ); 19 | 20 | var style = { 21 | height: this.props.height, 22 | overflow: 'hidden' 23 | }; 24 | 25 | return this.transferPropsTo( 26 |
27 | {React.isValidComponent(this.props.row) ? 28 | this.props.row : 29 | } 30 |
31 | ); 32 | }, 33 | 34 | renderCells() { 35 | var cells = []; 36 | var lockedCells = []; 37 | 38 | for (var i = 0, len = this.props.columns.length; i < len; i++) { 39 | var column = this.props.columns[i]; 40 | var cell = ( 41 | 51 | ); 52 | if (column.locked) { 53 | lockedCells.push(cell); 54 | } else { 55 | cells.push(cell); 56 | } 57 | } 58 | 59 | return cells.concat(lockedCells); 60 | }, 61 | 62 | renderCell(props) { 63 | if (React.isValidComponent(this.props.cellRenderer)) { 64 | return cloneWithProps(this.props.cellRenderer, props); 65 | } else { 66 | return this.props.cellRenderer(props); 67 | } 68 | }, 69 | 70 | getDefaultProps() { 71 | return { 72 | cellRenderer: Cell 73 | }; 74 | }, 75 | 76 | shouldComponentUpdate(nextProps) { 77 | return nextProps.columns !== this.props.columns || 78 | nextProps.row !== this.props.row || 79 | nextProps.height !== this.props.height; 80 | }, 81 | 82 | setScrollLeft(scrollLeft) { 83 | for (var i = 0, len = this.props.columns.length; i < len; i++) { 84 | if (this.props.columns[i].locked) { 85 | this.refs[i].setScrollLeft(scrollLeft); 86 | } 87 | } 88 | } 89 | }); 90 | 91 | module.exports = Row; 92 | -------------------------------------------------------------------------------- /lib/ScrollShim.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | * @copyright Prometheus Research, LLC 2014 4 | */ 5 | 'use strict'; 6 | 7 | var ScrollShim = { 8 | 9 | appendScrollShim() { 10 | if (!this._scrollShim) { 11 | var size = this._scrollShimSize(); 12 | var shim = document.createElement('div'); 13 | shim.classList.add('react-grid-ScrollShim'); 14 | shim.style.position = 'absolute'; 15 | shim.style.top = 0; 16 | shim.style.left = 0; 17 | shim.style.width = `${size.width}px`; 18 | shim.style.height = `${size.height}px`; 19 | this.getDOMNode().appendChild(shim); 20 | this._scrollShim = shim; 21 | } 22 | this._scheduleRemoveScrollShim(); 23 | }, 24 | 25 | _scrollShimSize() { 26 | return { 27 | width: this.props.width, 28 | height: this.props.length * this.props.rowHeight 29 | }; 30 | }, 31 | 32 | _scheduleRemoveScrollShim() { 33 | if (this._scheduleRemoveScrollShimTimer) { 34 | clearTimeout(this._scheduleRemoveScrollShimTimer); 35 | } 36 | this._scheduleRemoveScrollShimTimer = setTimeout( 37 | this._removeScrollShim, 200); 38 | }, 39 | 40 | _removeScrollShim() { 41 | if (this._scrollShim) { 42 | this._scrollShim.parentNode.removeChild(this._scrollShim); 43 | this._scrollShim = undefined; 44 | } 45 | } 46 | }; 47 | 48 | module.exports = ScrollShim; 49 | -------------------------------------------------------------------------------- /lib/Viewport.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | * @copyright Prometheus Research, LLC 2014 4 | */ 5 | 'use strict'; 6 | 7 | var React = require('react'); 8 | var getWindowSize = require('./getWindowSize'); 9 | var DOMMetrics = require('./DOMMetrics'); 10 | var Canvas = require('./Canvas'); 11 | 12 | var min = Math.min; 13 | var max = Math.max; 14 | var floor = Math.floor; 15 | var ceil = Math.ceil; 16 | 17 | var ViewportScroll = { 18 | mixins: [DOMMetrics.MetricsMixin], 19 | 20 | DOMMetrics: { 21 | viewportHeight() { 22 | return this.getDOMNode().offsetHeight; 23 | } 24 | }, 25 | 26 | propTypes: { 27 | rowHeight: React.PropTypes.number, 28 | length: React.PropTypes.number.isRequired 29 | }, 30 | 31 | getDefaultProps() { 32 | return { 33 | rowHeight: 30 34 | }; 35 | }, 36 | 37 | getInitialState() { 38 | return this.getGridState(this.props); 39 | }, 40 | 41 | getGridState(props) { 42 | var height = this.state && this.state.height ? 43 | this.state.height : 44 | getWindowSize().height; 45 | var renderedRowsCount = ceil(height / props.rowHeight); 46 | return { 47 | displayStart: 0, 48 | displayEnd: renderedRowsCount * 2, 49 | height: height, 50 | scrollTop: 0, 51 | scrollLeft: 0 52 | }; 53 | }, 54 | 55 | updateScroll(scrollTop, scrollLeft, height, rowHeight, length) { 56 | var renderedRowsCount = ceil(height / rowHeight); 57 | 58 | var visibleStart = floor(scrollTop / rowHeight); 59 | 60 | var visibleEnd = min( 61 | visibleStart + renderedRowsCount, 62 | length); 63 | 64 | var displayStart = max( 65 | 0, 66 | visibleStart - renderedRowsCount * 2); 67 | 68 | var displayEnd = min( 69 | visibleStart + renderedRowsCount * 2, 70 | length); 71 | 72 | var nextScrollState = { 73 | visibleStart, 74 | visibleEnd, 75 | displayStart, 76 | displayEnd, 77 | height, 78 | scrollTop, 79 | scrollLeft 80 | }; 81 | 82 | this.setState(nextScrollState); 83 | }, 84 | 85 | metricsUpdated() { 86 | var height = this.DOMMetrics.viewportHeight(); 87 | if (height) { 88 | this.updateScroll( 89 | this.state.scrollTop, 90 | this.state.scrollLeft, 91 | height, 92 | this.props.rowHeight, 93 | this.props.length 94 | ); 95 | } 96 | }, 97 | 98 | componentWillReceiveProps(nextProps) { 99 | if (this.props.rowHeight !== nextProps.rowHeight) { 100 | this.setState(this.getGridState(nextProps)); 101 | } else if (this.props.length !== nextProps.length) { 102 | this.updateScroll( 103 | this.state.scrollTop, 104 | this.state.scrollLeft, 105 | this.state.height, 106 | nextProps.rowHeight, 107 | nextProps.length 108 | ); 109 | } 110 | } 111 | }; 112 | 113 | var Viewport = React.createClass({ 114 | mixins: [ViewportScroll], 115 | 116 | render() { 117 | var style = { 118 | padding: 0, 119 | bottom: 0, 120 | left: 0, 121 | right: 0, 122 | overflow: 'hidden', 123 | position: 'absolute', 124 | top: this.props.rowHeight 125 | }; 126 | return ( 127 |
130 | 150 |
151 | ); 152 | }, 153 | 154 | getScroll() { 155 | return this.refs.canvas.getScroll(); 156 | }, 157 | 158 | onScroll({scrollTop, scrollLeft}) { 159 | this.updateScroll( 160 | scrollTop, scrollLeft, 161 | this.state.height, 162 | this.props.rowHeight, 163 | this.props.length 164 | ); 165 | 166 | if (this.props.onScroll) { 167 | this.props.onScroll({scrollTop, scrollLeft}); 168 | } 169 | }, 170 | 171 | setScrollLeft(scrollLeft) { 172 | this.refs.canvas.setScrollLeft(scrollLeft); 173 | } 174 | }); 175 | 176 | module.exports = Viewport; 177 | -------------------------------------------------------------------------------- /lib/copyProperties.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-2014 Facebook, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @providesModule copyProperties 17 | */ 18 | 'use strict'; 19 | 20 | /** 21 | * Copy properties from one or more objects (up to 5) into the first object. 22 | * This is a shallow copy. It mutates the first object and also returns it. 23 | * 24 | * NOTE: `arguments` has a very significant performance penalty, which is why 25 | * we don't support unlimited arguments. 26 | */ 27 | function copyProperties(obj, a, b, c, d, e, f) { 28 | obj = obj || {}; 29 | 30 | if (process.env.NODE_ENV) { 31 | if (f) { 32 | throw new Error('Too many arguments passed to copyProperties'); 33 | } 34 | } 35 | 36 | var args = [a, b, c, d, e]; 37 | var ii = 0, v; 38 | while (args[ii]) { 39 | v = args[ii++]; 40 | for (var k in v) { 41 | obj[k] = v[k]; 42 | } 43 | 44 | // IE ignores toString in object iteration.. See: 45 | // webreflection.blogspot.com/2007/07/quick-fix-internet-explorer-and.html 46 | if (v.hasOwnProperty && v.hasOwnProperty('toString') && 47 | (typeof v.toString != 'undefined') && (obj.toString !== v.toString)) { 48 | obj.toString = v.toString; 49 | } 50 | } 51 | 52 | return obj; 53 | } 54 | 55 | module.exports = copyProperties; 56 | -------------------------------------------------------------------------------- /lib/emptyFunction.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-2014 Facebook, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @providesModule emptyFunction 17 | */ 18 | 'use strict'; 19 | 20 | var copyProperties = require('./copyProperties'); 21 | 22 | function makeEmptyFunction(arg) { 23 | return function() { 24 | return arg; 25 | }; 26 | } 27 | 28 | /** 29 | * This function accepts and discards inputs; it has no side effects. This is 30 | * primarily useful idiomatically for overridable function endpoints which 31 | * always need to be callable, since JS lacks a null-call idiom ala Cocoa. 32 | */ 33 | function emptyFunction() {} 34 | 35 | copyProperties(emptyFunction, { 36 | thatReturns: makeEmptyFunction, 37 | thatReturnsFalse: makeEmptyFunction(false), 38 | thatReturnsTrue: makeEmptyFunction(true), 39 | thatReturnsNull: makeEmptyFunction(null), 40 | thatReturnsThis: function() { return this; }, 41 | thatReturnsArgument: function(arg) { return arg; } 42 | }); 43 | 44 | module.exports = emptyFunction; 45 | -------------------------------------------------------------------------------- /lib/getScrollbarSize.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var size; 4 | 5 | function getScrollbarSize() { 6 | if (size === undefined) { 7 | 8 | var outer = document.createElement('div'); 9 | outer.style.width = '50px'; 10 | outer.style.height = '50px'; 11 | outer.style.overflowY = 'scroll'; 12 | outer.style.position = 'absolute'; 13 | outer.style.top = '-200px'; 14 | outer.style.left = '-200px'; 15 | 16 | var inner = document.createElement('div'); 17 | inner.style.height = '100px'; 18 | inner.style.width = '100%'; 19 | 20 | outer.appendChild(inner); 21 | document.body.appendChild(outer); 22 | 23 | var outerWidth = outer.offsetWidth; 24 | var innerWidth = inner.offsetWidth; 25 | 26 | document.body.removeChild(outer); 27 | 28 | size = outerWidth - innerWidth; 29 | } 30 | 31 | return size; 32 | } 33 | 34 | module.exports = getScrollbarSize; 35 | -------------------------------------------------------------------------------- /lib/getWindowSize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | * @copyright Prometheus Research, LLC 2014 4 | */ 5 | 'use strict'; 6 | 7 | /** 8 | * Return window's height and width 9 | * 10 | * @return {Object} height and width of the window 11 | */ 12 | function getWindowSize() { 13 | var width = window.innerWidth; 14 | var height = window.innerHeight; 15 | 16 | if (!width || !height) { 17 | width = document.documentElement.clientWidth; 18 | height = document.documentElement.clientHeight; 19 | } 20 | 21 | if (!width || !height) { 22 | width = document.body.clientWidth; 23 | height = document.body.clientHeight; 24 | } 25 | 26 | return {width, height}; 27 | } 28 | 29 | module.exports = getWindowSize; 30 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | * @copyright Prometheus Research, LLC 2014 4 | */ 5 | 'use strict'; 6 | 7 | var Grid = require('./Grid'); 8 | var Row = require('./Row'); 9 | var Cell = require('./Cell'); 10 | 11 | module.exports = Grid; 12 | module.exports.Row = Row; 13 | module.exports.Cell = Cell; 14 | -------------------------------------------------------------------------------- /lib/invariant.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-2014 Facebook, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @providesModule invariant 17 | */ 18 | 19 | "use strict"; 20 | 21 | /** 22 | * Use invariant() to assert state which your program assumes to be true. 23 | * 24 | * Provide sprintf-style format (only %s is supported) and arguments 25 | * to provide information about what broke and what you were 26 | * expecting. 27 | * 28 | * The invariant message will be stripped in production, but the invariant 29 | * will remain to ensure logic does not differ in production. 30 | */ 31 | 32 | var invariant = function(condition, format, a, b, c, d, e, f) { 33 | if (process.env.NODE_ENV) { 34 | if (format === undefined) { 35 | throw new Error('invariant requires an error message argument'); 36 | } 37 | } 38 | 39 | if (!condition) { 40 | var error; 41 | if (format === undefined) { 42 | error = new Error( 43 | 'Minified exception occurred; use the non-minified dev environment ' + 44 | 'for the full error message and additional helpful warnings.' 45 | ); 46 | } else { 47 | var args = [a, b, c, d, e, f]; 48 | var argIndex = 0; 49 | error = new Error( 50 | 'Invariant Violation: ' + 51 | format.replace(/%s/g, function() { return args[argIndex++]; }) 52 | ); 53 | } 54 | 55 | error.framesToPop = 1; // we don't care about invariant's own frame 56 | throw error; 57 | } 58 | }; 59 | 60 | module.exports = invariant; 61 | -------------------------------------------------------------------------------- /lib/shallowCloneObject.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | * @copyright Prometheus Research, LLC 2014 4 | */ 5 | 'use strict'; 6 | 7 | function shallowCloneObject(obj) { 8 | var result = {}; 9 | for (var k in obj) { 10 | if (obj.hasOwnProperty(k)) { 11 | result[k] = obj[k]; 12 | } 13 | } 14 | return result; 15 | } 16 | 17 | module.exports = shallowCloneObject; 18 | -------------------------------------------------------------------------------- /lib/shallowEqual.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | * @copyright Prometheus Research, LLC 2014 4 | */ 5 | 'use strict'; 6 | 7 | function shallowEqual(a, b) { 8 | if (a === b) { 9 | return true; 10 | } 11 | 12 | var k; 13 | 14 | for (k in a) { 15 | if (a.hasOwnProperty(k) && 16 | (!b.hasOwnProperty(k) || a[k] !== b[k])) { 17 | return false; 18 | } 19 | } 20 | 21 | for (k in b) { 22 | if (b.hasOwnProperty(k) && !a.hasOwnProperty(k)) { 23 | return false; 24 | } 25 | } 26 | 27 | return true; 28 | } 29 | 30 | module.exports = shallowEqual; 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-grid", 3 | "version": "0.1.0", 4 | "description": "Data grid fro React", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "make test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/prometheusresearch/react-grid.git" 12 | }, 13 | "keywords": [ 14 | "react", 15 | "grid" 16 | ], 17 | "dependencies": { 18 | "reactify": "^0.14.0" 19 | }, 20 | "peerDependencies": { 21 | "react": "^0.11.1" 22 | }, 23 | "devDependencies": { 24 | "browserify": "^5.9.1", 25 | "eslint-jsx": "git+https://github.com/andreypopp/eslint#jsx", 26 | "mocha": "^1.18.2", 27 | "mochify": "^0.11.3", 28 | "react": "^0.11.1", 29 | "sinon": "^1.9.1" 30 | }, 31 | "browserify": { 32 | "transform": [ 33 | [ 34 | "reactify", 35 | { 36 | "es6": true 37 | } 38 | ] 39 | ] 40 | }, 41 | "author": "Prometheus Research", 42 | "license": "MIT", 43 | "bugs": { 44 | "url": "https://github.com/prometheusresearch/react-grid/issues" 45 | }, 46 | "homepage": "https://github.com/prometheusresearch/react-grid" 47 | } 48 | -------------------------------------------------------------------------------- /standalone/Makefile: -------------------------------------------------------------------------------- 1 | .DELETE_ON_ERROR: 2 | .INTERMEDIATE: react-grid.prod.js 3 | 4 | TARGETS = react-grid.js react-grid.min.js 5 | 6 | PATH := ../node_modules/.bin:$(PATH) 7 | 8 | build: $(TARGETS) 9 | 10 | react-grid.js: 11 | $(call browserify,development) 12 | 13 | react-grid.prod.js: 14 | $(call browserify,production) 15 | 16 | react-grid.min.js: react-grid.prod.js 17 | @cat $< | uglifyjs -cm > $@ 18 | 19 | clean: 20 | @rm -f $(TARGETS) 21 | 22 | define browserify 23 | @NODE_ENV=$(1) browserify -t [ reactify --es6] . \ 24 | | sed -E 's/function\(require/function(__browserify__/g' \ 25 | | sed -E 's/require\(/__browserify__(/g' \ 26 | >> $@ 27 | endef 28 | -------------------------------------------------------------------------------- /standalone/index.js: -------------------------------------------------------------------------------- 1 | ;(function (root, factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | define(['react'], factory); 4 | } else { 5 | root.ReactGrid = factory(root.React); 6 | } 7 | })(window, function(React) { 8 | return require('./lib/'); 9 | }); 10 | -------------------------------------------------------------------------------- /standalone/lib: -------------------------------------------------------------------------------- 1 | ../lib -------------------------------------------------------------------------------- /standalone/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-grid-standalone", 3 | "main": "./index.js", 4 | "browserify": { 5 | "transform": [ 6 | [ 7 | "browserify-shim", 8 | {"global": true} 9 | ], 10 | [ 11 | "envify", 12 | {"global": true} 13 | ] 14 | ] 15 | }, 16 | "browserify-shim": { 17 | "react": "global:window.React", 18 | "react/addons": "global:window.React" 19 | }, 20 | "devDependencies": { 21 | "browserify": "^3.33.0", 22 | "browserify-shim": "^3.3.2", 23 | "derequire": "^0.8.0", 24 | "envify": "^1.2.1", 25 | "uglify-js": "^2.4.13" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /themes/bootstrap.less: -------------------------------------------------------------------------------- 1 | @react-grid-shadow: 0px 0px 4px 0px @table-border-color; 2 | @react-grid-shadow-locked: none; 3 | @react-grid-scrollbar-color: @gray-light; 4 | 5 | .react-grid-Grid { 6 | background-color: @body-bg; 7 | border: 1px solid @table-border-color; 8 | } 9 | 10 | .react-grid-Canvas { 11 | background-color: @body-bg; 12 | } 13 | 14 | .react-grid-Cell { 15 | background-color: @body-bg; 16 | padding: @table-cell-padding; 17 | border-top: 1px solid @table-border-color; 18 | } 19 | 20 | .react-grid-Cell__value { 21 | white-space: nowrap; 22 | text-overflow: ellipsis; 23 | overflow: hidden; 24 | position: relative; 25 | top: 50%; 26 | transform: translateY(-50%); 27 | } 28 | 29 | .react-grid-Row { 30 | &:hover .react-grid-Cell { 31 | background-color: darken(@body-bg, 5%); 32 | } 33 | } 34 | 35 | .react-grid-Cell--locked:last-of-type { 36 | border-right: 1px solid @table-border-color; 37 | box-shadow: @react-grid-shadow-locked; 38 | } 39 | 40 | .react-grid-Header { 41 | box-shadow: @react-grid-shadow; 42 | background: @gray-lighter; 43 | } 44 | 45 | .react-grid-Header--resizing { 46 | cursor: ew-resize; 47 | } 48 | 49 | .react-grid-HeaderRow { 50 | .user-select(none); 51 | border-bottom: 1px solid @table-border-color; 52 | } 53 | 54 | .react-grid-HeaderCell { 55 | .user-select(none); 56 | background: @gray-lighter; 57 | padding: @table-cell-padding; 58 | font-weight: bold; 59 | border-right: 1px solid @table-border-color; 60 | } 61 | 62 | .react-grid-HeaderCell__value { 63 | white-space: nowrap; 64 | text-overflow: ellipsis; 65 | overflow: hidden; 66 | position: relative; 67 | top: 50%; 68 | transform: translateY(-50%); 69 | } 70 | 71 | .react-grid-HeaderCell__resizeHandle:hover { 72 | cursor: ew-resize; 73 | background: @table-border-color; 74 | } 75 | 76 | .react-grid-HeaderCell--locked:last-of-type { 77 | box-shadow: @react-grid-shadow-locked; 78 | } 79 | 80 | .react-grid-HeaderCell--resizing { 81 | .react-grid-HeaderCell__resizeHandle { 82 | background: @table-border-color; 83 | } 84 | } 85 | 86 | .react-grid-ScrollShim { 87 | z-index: 10002; 88 | } 89 | --------------------------------------------------------------------------------