diff --git a/.dockerignore b/.dockerignore index 1269488..ecdf3cc 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,3 @@ data +**/node_modules +**/*.pyc diff --git a/.github/workflows/docker-hub.yml b/.github/workflows/docker-hub.yml new file mode 100644 index 0000000..7164b15 --- /dev/null +++ b/.github/workflows/docker-hub.yml @@ -0,0 +1,22 @@ +name: Build and push *base* image to Docker Hub + +on: + push: + # Don't waste time building every push: consider only tags, which are + # usually releases + tags: + - '*' + +jobs: + docker-base-image-build-push: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: mr-smithers-excellent/docker-build-push@v5 + with: + image: kobotoolbox/reports_base + tags: latest + registry: docker.io + dockerfile: Dockerfile.base + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_PASSWORD }} diff --git a/.gitignore b/.gitignore index 1145489..8e8af47 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ koboreports/static/assets /media/ /koboreports/static/login.css node_modules +huey.db* diff --git a/Dockerfile b/Dockerfile index 81f6e0c..b47d1c5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,18 +4,31 @@ FROM kobotoolbox/reports_base # koboreports # ############### -COPY . /app/ +# Freshen dependencies in case they've changed since the base +# image was built +COPY environment.yml /app/ +COPY jsapp/package.json jsapp/package-lock.json /app/jsapp/ +RUN conda env update --prune +RUN cd jsapp && npm install -WORKDIR /app/demo -RUN grunt build -WORKDIR /app +# Include all remaining source files except for jsapp/node_modules, which might +# be peculiar to a developer's host environment +COPY . /tmp/app/ +RUN (test \! -e /tmp/app/jsapp/node_modules || rm -r /tmp/app/jsapp/node_modules) && \ + (shopt -s dotglob && cp -a /tmp/app/* /app/ && rm -r /tmp/app) +# Build the front end +RUN cd jsapp && npm run build + +# Run Python unit tests RUN source activate koboreports && \ - python manage.py test --noinput + SECRET_KEY=bogus KPI_API_KEY=bogus ALLOWED_HOSTS=bogus python manage.py test --noinput # Persistent storage of uploaded XLSForms! +# In production, you should also configure persistent storage in Dokku: +# https://dokku.com/docs~v0.24.7/advanced-usage/persistent-storage/#persistent-storage VOLUME ["/app/media"] # As of Dokku 0.5.0, no ports should be `EXPOSE`d; see # http://dokku.viewdocs.io/dokku/deployment/methods/dockerfiles/#exposed-ports -CMD ./run.sh # calls `manage.py migrate` and `collectstatic` +CMD ./run.sh # calls `manage.py migrate` and `collectstatic` diff --git a/Dockerfile.base b/Dockerfile.base index 05caa47..7218385 100644 --- a/Dockerfile.base +++ b/Dockerfile.base @@ -1,66 +1,53 @@ -FROM ubuntu:xenial - -########### -# apt-get # -########### - -ADD https://deb.nodesource.com/setup_6.x /tmp/setup_6.x.bash - -RUN bash /tmp/setup_6.x.bash && \ - apt-get install -y --no-install-recommends \ - build-essential \ - curl \ - libgmp10 \ - libpq-dev \ - libxrender1 \ - nodejs \ - texlive-full \ - wget +FROM node:14 + +# Docker default of `/bin/sh` doesn't support `source` +SHELL ["/bin/bash", "-c"] + +# Add Conda repository +# https://docs.conda.io/projects/conda/en/latest/user-guide/install/rpm-debian.html +RUN curl https://repo.anaconda.com/pkgs/misc/gpgkeys/anaconda.asc | gpg --dearmor > conda.gpg && \ + install -o root -g root -m 644 conda.gpg /usr/share/keyrings/conda-archive-keyring.gpg && \ + echo "deb [arch=amd64 signed-by=/usr/share/keyrings/conda-archive-keyring.gpg] https://repo.anaconda.com/pkgs/misc/debrepo/conda stable main" > /etc/apt/sources.list.d/conda.list + +# Install Conda and other OS-level dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + conda \ + pandoc \ + libgmp10 \ + libpq-dev \ + libxrender1 \ + texlive-full # Work around a font rendering problem; see # https://github.com/kobotoolbox/reports/issues/136 +# FIXME: figure out what subset of `texlive-full` is actually needed +# Do not `apt-get autoremove` after this(!) since it would remove necessary +# packages RUN apt-get remove -y tex-gyre -########## -# pandoc # -########## - -# TODO: Remove --no-check-certificate -RUN wget --no-check-certificate https://github.com/jgm/pandoc/releases/download/1.15.0.6/pandoc-1.15.0.6-1-amd64.deb -O pandoc.deb && \ - dpkg -i pandoc.deb && \ - rm pandoc.deb - -############################## -# conda install Python and R # -############################## - -RUN wget http://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh && \ - chmod +x miniconda.sh && \ - ./miniconda.sh -b && \ - rm miniconda.sh -ENV PATH /root/miniconda2/bin:$PATH +# `apt-get install conda` does not actually put Conda on the PATH +ENV PATH /opt/conda/bin:$PATH RUN conda update --yes conda RUN mkdir /app WORKDIR /app -# https://www.continuum.io/content/conda-data-science +# Copy only the files that define dependencies, not all the source files, to +# avoid unnecessarily invalidating the Docker layer cache COPY environment.yml /app/ -RUN conda env create +COPY jsapp/package.json jsapp/package-lock.json /app/jsapp/ -# http://stackoverflow.com/a/25423366/3756632 -# need this for "source activate" commands -RUN rm /bin/sh && ln -s /bin/bash /bin/sh +# Install Python and R dependencies +RUN conda env create -# R libraries not available through conda +# Install R libraries not available through Conda RUN source activate koboreports && \ - Rscript -e "install.packages('pander', repos='http://cran.rstudio.com/', type='source')" -e "library(pander)" + Rscript -e "install.packages('pander', repos='http://cran.rstudio.com/', type='source')" \ + -e "library(pander)" -############################# -# install node dependencies # -############################# +# At this time, the `node:14` image includes npm 6, but npm pesters us about +# upgrading to 7. Oblige it: +RUN npm install -g npm@7 -RUN npm install -g grunt-cli -COPY demo/package.json /tmp/package.json -RUN cd /tmp && npm install && mkdir /app/demo && \ - cp -a /tmp/node_modules /app/demo/ +# Install Node.js dependencies +RUN cd jsapp && npm install diff --git a/README.md b/README.md index f53b5a8..5853524 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,162 @@ +Last updated: June 29, 2021 + # What is this? -This is a custom application that runs at https://data.equitytool.org. It relies on https://kf.kobotoolbox.org for authentication, form deployment, and submission collection/storage. +`reports` is Django project for compiling dynamic reports using R Markdown and +KoBoToolbox. It is built on the R package knitr: http://yihui.name/knitr/. + +This code currently runs on https://data.equitytool.org and relies on +https://kf.kobotoolbox.org for authentication, form deployment, and submission +collection/storage. + +# Administrative Reports + +To create an administrative report that lists all the users and projects stored +in the database: + +1. Log into https://data.equitytool.org/admin/ as a superuser; +1. Click "+ Add" next to "Admin stats report tasks"; +1. Click "SAVE"; +1. A screen listing "new report" first as well as previous reports below will + appear; +1. Refresh this page every few minutes until "new report" changes into + "complete report"; +1. Once that happens, click "complete report"; +1. Finally, click "equitytool_admin_stats....zip" (to the right of "Result") + to download the ZIP file containing the statistics. + +# Application Structure (Django Models) + +## `equitytool.Form` + +An administrator-defined form that regular users can select when they create +new projects. The form is then sent to the regular user's linked KoBoToolbox +account, creating a new project that, in turn, is referenced by a +`reporter.Rendering` + +## `reporter.Template` + +These R Markdown templates transform collected data from its raw state into +formatted reports (narrative, tables, charts). They are available read-only to +all users, but may only be changed by superusers via the +https://data.equitytool.org/admin/ (Django Admin) interface. + +## `reporter.Rendering` + +Connects the R Markdown `reporter.Template`s to data collected with +KoBoToolbox. There is one instance of this for each user-created project. -## What else is it? +## `reporter.UserExternalApiToken` / User Authentication -`reports` is a Django project for compiling dynamic reports. It is -built on the R package [knitr][knitr]. +This application uses a [special +interface](https://github.com/kobotoolbox/kpi/pull/368/files) +of kf.kobotoolbox.org to create KoBoToolbox users without email confirmation. +Once a user registers in this way, they are then authenticated by sending their +credentials over HTTPS to the KoBoToolbox server (see +`reporter.KoboApiAuthBackend`). If the credentials are correct, KoBoToolbox +returns an API key, which this application then stores in +`reporter.UserExternalApiToken`. That key then authenticates subsequent +requests to deploy forms and retrieve submissions. -# Features +There are also local-only users whose passwords (hashed) are stored directly in +this application's database. Superusers are an example of this. These users +have privileges to administer this application but not the connected +KoBoToolbox instance. Local-only users also cannot create data collection +projects as they have no access to KoBoToolbox. -Report templates are stored in the database. See -`reporter.models.Template.rmd`. +## `equitytool.AdminStatsReportTask` -When rendering a report we can pull data from any URL. See -`reporter.tests.TestRendering.test_url`. +This model allows [administrative reports](#administrative-reports) to be +generated in the background where they are not subject to the same time limits +as ordinary web application requests. -Project is dockerized so it's easy to deploy. See `Dockerfile`. +# R Markdown Templates Mustache tags are supported so we can return a warning message if a deployment has fewer than 150 responses. See `reporter.tests.TestRendering.test_warning`. -# Variables passed to Rmd templates +## Variables Available in Templates * `rendering__name`: `Rendering.name`, i.e. the user's name for the project * `form__name`: `Form.name`, e.g. `Tajikistan (DHS 2012)` * `request__show_urban`: the value of `?show_urban=` in the URL used to request the report +# Known Issues + +* Tables in DOCX exports do not appear properly in LibreOffice: see + https://github.com/jgm/pandoc/issues/515 + +# Production installation with [Dokku](https://dokku.com/) + +For this example, we're using an AWS t3.small EC2 instance, which provides 2 +GiB of RAM, along with a 40 GiB EBS root volume. It is running Ubuntu 20.04. + +1. Due to the limited amount of RAM, add a 5 GiB swap file: + ``` + sudo fallocate --length 5G /swapfile + sudo mkswap /swapfile + sudo chmod 600 /swapfile + ``` + Add the following to `/etc/fstab` to enable swap at each boot: + ``` + /swapfile none swap sw 0 0 + ``` + Enable the swap now: + ``` + sudo swapon -a + ``` +1. Install Dokku: + ``` + wget https://raw.githubusercontent.com/dokku/dokku/v0.24.7/bootstrap.sh + sudo DOKKU_TAG=v0.24.7 bash bootstrap.sh + sudo reboot + ``` +1. Create the Dokku application: + ``` + dokku apps:create data.equitytool.org + ``` +1. Configure the new application: + ``` + dokku config:set data.equitytool.org ALLOWED_HOSTS='data.equitytool.org' + dokku config:set data.equitytool.org SECRET_KEY='[your randomly-generated Django secret key]' + # Optional Python exception logging + dokku config:set data.equitytool.org RAVEN_DSN='[your Sentry DSN]' + ``` + Connect the application to an instance of KoBoToolbox; see [Development + _without_ Dokku](#development-without-dokku) for more information: + ``` + dokku config:set data.equitytool.org KPI_URL='https://kf.kobotoolbox.org' + dokku config:set data.equitytool.org KPI_API_KEY='[your kpi authorized application key]' + ``` +1. Enable TLS (HTTPS): + ``` + dokku letsencrypt:enable data.equitytool.org + ``` +1. Install Postgres and link it with the new application: + ``` + sudo dokku plugin:install https://github.com/dokku/dokku-postgres.git + dokku postgres:create reports + dokku postgres:link reports data.equitytool.org + ``` +1. Add persistent storage for media uploads: + ``` + dokku storage:mount data.equitytool.org /var/lib/dokku/data/storage/data.equitytool.org:/app/media + ``` +1. Follow the [Dokku + documentation](https://dokku.com/docs/deployment/application-deployment/#deploy-the-app) + to deploy the application code from your local machine to this new server + using `git push`. This relies on the [base image](Dockerfile.base) having + already been built and pushed to [Docker + Hub](https://hub.docker.com/r/kobotoolbox/reports_base/tags?page=1&ordering=last_updated) + by [GitHub Actions](.github/workflows/docker-hub.yml). Reducing the size of + this base image (currently over 4 GiB) would be a nice improvement. # Development _without_ Dokku This application requires a working instance of KoBoToolbox to run. See -[kobo-docker](https://github.com/kobotoolbox/kobo-docker) for instructions +[kobo-install](https://github.com/kobotoolbox/kobo-install) for instructions on how to install such an instance. 1. Go to `https://[YOUR KPI DOMAIN]/admin/kpi/authorizedapplication/` (you will @@ -40,82 +164,38 @@ on how to install such an instance. 1. Click `Add authorized application`; 1. Name your application and note the randomly-generated key (or enter your own); - 1. **NB:** To escape a `$` in the key, - [double it to `$$`](https://github.com/docker/compose/issues/3427). + - **NB:** To escape a `$` in the key, [double it to + `$$`](https://github.com/docker/compose/issues/3427). 1. Click `Save`; 1. Edit `docker-compose.yml` for this `reports` application: 1. Set the `KPI_API_KEY` environment variable equal to the application key generated above; 1. Set `KPI_URL` to `https://[YOUR KPI DOMAIN]/`; + * If you are using a locally-hosted KoBoToolbox instance, you may need + to configure `extra_hosts` as well. + 1. Set `ALLOWED_HOSTS` to match the hostname of your `reports` instance; + see https://docs.djangoproject.com/en/3.2/ref/settings/#allowed-hosts. 1. Execute `docker-compose pull`; -1. Execute `docker build -t kobotoolbox/reports_base -f Dockerfile.base .` (this is a slow process); +1. Execute `docker build -t kobotoolbox/reports_base -f Dockerfile.base .` + (this is a slow process); 1. Supplicate before the gods of JavaScript and execute `docker-compose build`; 1. Execute `docker-compose up -d postgres`; 1. Execute `docker-compose logs -f`; 1. Wait for the Postgres container to settle as indicated by the logs; 1. Interrupt (CTRL+C) `docker-compose logs`; -1. Start the application with `docker-compose up -d`; +1. Start the web application with `docker-compose up -d`; 1. Get a shell inside the application container by running `docker-compose exec koboreports bash`; -1. Set the domain for the Django sites framework to match the hostname - (or IP address) of your development machine: - 1. (Inside the application container) `source activate koboreports`; - 1. `./manage.py shell`; - 1. `from django.contrib.sites.models import *`; - 1. `s = Site.objects.first()`; - 1. `s.domain = s.name = 'your.reports.domain'` (include `:port` if - necessary); - 1. `s.save()`; - 1. `exit()`; -1. Load some sample `Form`s, if desired: - 1. (Inside the application container) `source activate koboreports`; - 1. `./manage.py loaddata dev/sample-forms.json`; -1. You may want to create a superuser: - 1. (Inside the application container) `source activate koboreports`; - 1. `./manage.py createsuperuser`. - -# Backburner - -On each compilation of a report, it would be nice to save the -corresponding CSV file. That way when the report is recompiled we can -pull only the new / updated data from the API to get the latest -data. I'm not going to worry about setting this up right now, but I'll -keep it in mind in my design decisions. - -I am going to ignore different user roles for the moment. It seems -like there is a large NGO that wants to write reports, and then there -are satellite offices collecting data that will want to view the -reports. For the moment I'm not going to worry too much about -supporting different users and groups. - -We will want to share sessions with the other kobo projects on the -server, this will allow users to log in once. - -# My Notes - -Right now I am deploying this web application using [dokku][dokku]. To -see an instance of this application running go to -[koboreports][koboreports]. - -Here's how I set up an admin account using dokku: - -1. SSH into AWS server. -2. Find name of the docker container that is running the application - using `docker ps`. -3. Open a bash terminal inside that docker container: - - docker exec -it {{ container_name }} bash - -4. Use django's management command to create a super user: - - python manage.py createsuperuser - -One thing to keep in mind when deploying with dokku is every time you -deploy, a new docker container is built. If you are using sqlite to -store data inside a container that data will be lost when you redeploy -the application. I am using postgres to persist data across -deployments. - -[knitr]: http://yihui.name/knitr/ -[dokku]: http://progrium.viewdocs.io/dokku/ -[koboreports]: http://koboreports.hbs-rcs.org/ +1. If desired, load some sample `Form`s into the database: + ``` + # Inside the application container + source activate koboreports + ./manage.py loaddata dev/sample-forms.json + ``` +1. To access the Django Admin interface, you'll need a superuser account. + Create one now: + ``` + # Inside the application container + source activate koboreports + ./manage.py createsuperuser + ``` diff --git a/demo/.editorconfig b/demo/.editorconfig deleted file mode 100644 index c308ed0..0000000 --- a/demo/.editorconfig +++ /dev/null @@ -1,13 +0,0 @@ -# http://editorconfig.org -root = true - -[*] -indent_style = space -indent_size = 4 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -[*.md] -trim_trailing_whitespace = false diff --git a/demo/.eslintrc b/demo/.eslintrc deleted file mode 100644 index 7363235..0000000 --- a/demo/.eslintrc +++ /dev/null @@ -1,24 +0,0 @@ -{ - "plugins": [ - "react" - ], - "ecmaFeatures": { - "jsx": true, - "modules": true - }, - "env": { - "browser": true, - "amd": true, - "es6": true - }, - "rules": { - "quotes": [ 1, "single" ], - "no-undef": false, - "no-console": false, - "camelcase": false, - "global-strict": false, - "no-extra-semi": 1, - "comma-dangle": 0, - "no-underscore-dangle": false - } -} diff --git a/demo/.jshintrc b/demo/.jshintrc deleted file mode 100644 index 2f22258..0000000 --- a/demo/.jshintrc +++ /dev/null @@ -1,27 +0,0 @@ -{ - "node": true, - "browser": true, - "esnext": true, - "bitwise": true, - "camelcase": false, - "curly": true, - "eqeqeq": true, - "immed": true, - "indent": 2, - "latedef": true, - "newcap": true, - "noarg": true, - "quotmark": "false", - "regexp": true, - "undef": true, - "unused": false, - "strict": true, - "trailing": true, - "smarttabs": true, - "white": true, - "newcap": false, - "globals": { - "React": true - } -} - diff --git a/demo/.yo-rc.json b/demo/.yo-rc.json deleted file mode 100644 index dd8d057..0000000 --- a/demo/.yo-rc.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "generator-react-webpack": { - "app-name": "metricsUi", - "architecture": "reflux", - "styles-language": "scss", - "component-suffix": "js" - } -} \ No newline at end of file diff --git a/demo/Gruntfile.js b/demo/Gruntfile.js deleted file mode 100644 index bf33613..0000000 --- a/demo/Gruntfile.js +++ /dev/null @@ -1,136 +0,0 @@ -'use strict'; - -var mountFolder = function (connect, dir) { - return connect.static(require('path').resolve(dir)); -}; - -var webpackDistConfig = require('./webpack.dist.config.js'), - webpackDevConfig = require('./webpack.config.js'); - -module.exports = function (grunt) { - // Let *load-grunt-tasks* require everything - require('load-grunt-tasks')(grunt); - - // Read configuration from package.json - var pkgConfig = grunt.file.readJSON('package.json'); - - grunt.initConfig({ - pkg: pkgConfig, - - webpack: { - options: webpackDistConfig, - dist: { - cache: false - } - }, - - 'webpack-dev-server': { - options: { - hot: true, - port: 8000, - webpack: webpackDevConfig, - publicPath: '/assets/', - contentBase: './<%= pkg.src %>/' - }, - - start: { - keepAlive: true - } - }, - - connect: { - options: { - port: 8000 - }, - - dist: { - options: { - keepalive: true, - middleware: function (connect) { - return [ - mountFolder(connect, pkgConfig.dist) - ]; - } - } - } - }, - sass: { - dist: { - files: { - '../koboreports/static/login.css': [ - 'src/styles/MetricsUI.scss', - 'src/styles/Forms.scss', - 'src/styles/Login.scss' - ] - } - } - }, - - open: { - options: { - delay: 500 - }, - dev: { - path: 'http://localhost:<%= connect.options.port %>/webpack-dev-server/' - }, - dist: { - path: 'http://localhost:<%= connect.options.port %>/' - } - }, - - karma: { - unit: { - configFile: 'karma.conf.js' - } - }, - - copy: { - dist: { - files: [ - // includes files within path - { - flatten: true, - expand: true, - src: ['<%= pkg.src %>/*'], - dest: '<%= pkg.dist %>/', - filter: 'isFile' - }, - { - flatten: true, - expand: true, - src: ['<%= pkg.src %>/images/*'], - dest: '<%= pkg.dist %>/images/' - } - ] - } - }, - - clean: { - dist: { - files: [{ - dot: true, - src: [ - '<%= pkg.dist %>' - ] - }] - } - } - }); - - grunt.registerTask('serve', function (target) { - if (target === 'dist') { - return grunt.task.run(['build', 'open:dist', 'connect:dist']); - } - - grunt.task.run([ - 'open:dev', - 'webpack-dev-server' - ]); - }); - - grunt.registerTask('test', ['karma']); - - grunt.registerTask('build', ['clean', 'sass', 'copy', 'webpack']); - - grunt.registerTask('default', []); -}; diff --git a/demo/README.md b/demo/README.md deleted file mode 100644 index 109b2e1..0000000 --- a/demo/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Running the demo - - - CD into this demo directory - - with npm installed, run `npm install` - - run `grunt serve` diff --git a/demo/karma.conf.js b/demo/karma.conf.js deleted file mode 100644 index bb55214..0000000 --- a/demo/karma.conf.js +++ /dev/null @@ -1,84 +0,0 @@ -'use strict'; - -var path = require('path'); - -module.exports = function (config) { - config.set({ - basePath: '', - frameworks: ['jasmine'], - files: [ - 'test/helpers/pack/**/*.js', - 'test/helpers/react/**/*.js', - 'test/spec/components/**/*.js', - 'test/spec/stores/**/*.js', - 'test/spec/actions/**/*.js' - ], - preprocessors: { - 'test/helpers/createComponent.js': ['webpack'], - 'test/spec/components/**/*.js': ['webpack'], - 'test/spec/components/**/*.jsx': ['webpack'], - 'test/spec/stores/**/*.js': ['webpack'], - 'test/spec/actions/**/*.js': ['webpack'] - }, - webpack: { - cache: true, - module: { - loaders: [{ - test: /\.gif/, - loader: 'url-loader?limit=10000&mimetype=image/gif' - }, { - test: /\.jpg/, - loader: 'url-loader?limit=10000&mimetype=image/jpg' - }, { - test: /\.png/, - loader: 'url-loader?limit=10000&mimetype=image/png' - }, { - test: /\.(js|jsx)$/, - loader: 'babel-loader', - exclude: /node_modules/ - }, { - test: /\.scss/, - loader: 'style-loader!css-loader!sass-loader?outputStyle=expanded' - }, { - test: /\.css$/, - loader: 'style-loader!css-loader' - }, { - test: /\.woff/, - loader: 'url-loader?limit=10000&mimetype=application/font-woff' - }, { - test: /\.woff2/, - loader: 'url-loader?limit=10000&mimetype=application/font-woff2' - }] - }, - resolve: { - alias: { - 'styles': path.join(process.cwd(), './src/styles/'), - 'components': path.join(process.cwd(), './src/components/'), - 'stores': '../../../src/stores/', - 'actions': '../../../src/actions/', - 'helpers': path.join(process.cwd(), './test/helpers/') - } - } - }, - webpackMiddleware: { - noInfo: true, - stats: { - colors: true - } - }, - exclude: [], - port: 8080, - logLevel: config.LOG_INFO, - colors: true, - autoWatch: false, - browsers: ['PhantomJS'], - reporters: ['dots'], - captureTimeout: 60000, - singleRun: true, - plugins: [ - require('karma-webpack'), - require('karma-jasmine'), - require('karma-phantomjs-launcher') - ] - }); -}; diff --git a/demo/package.json b/demo/package.json deleted file mode 100644 index 50236ef..0000000 --- a/demo/package.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "name": "metricsui", - "version": "0.0.0", - "description": "", - "repository": "", - "private": true, - "src": "src", - "test": "test", - "dist": "dist", - "mainInput": "main", - "mainOutput": "main", - "dependencies": { - "normalize.css": "~3.0.3", - "react": "0.13.x", - "react-input-autosize": "^0.5.3", - "react-router": "0.13.x", - "react-select": "^0.6.11", - "react-tooltip": "^0.6.4", - "react-modal": "0.5.0", - "reflux": "^0.2.7" - }, - "devDependencies": { - "alertifyjs": "^1.5.0", - "babel": "^5.0.0", - "babel-loader": "^5.0.0", - "classnames": "^2.1.3", - "css-loader": "~0.9.0", - "eslint": "^0.21.2", - "eslint-loader": "^0.11.2", - "eslint-plugin-react": "^2.4.0", - "grunt": "~0.4.5", - "grunt-contrib-clean": "~0.6.0", - "grunt-contrib-connect": "~0.8.0", - "grunt-contrib-copy": "~0.5.0", - "grunt-karma": "~0.8.3", - "grunt-open": "~0.2.3", - "grunt-sass": "^2.0.0", - "grunt-webpack": "~1.0.8", - "jasmine-core": "^2.3.4", - "jquery": "^2.1.4", - "karma": "~0.12.21", - "karma-jasmine": "^0.3.5", - "karma-phantomjs-launcher": "~0.1.3", - "karma-script-launcher": "~0.1.0", - "karma-webpack": "^1.5.0", - "less-loader": "^2.2.1", - "load-grunt-tasks": "~0.6.0", - "md5": "^2.0.0", - "moment": "^2.10.6", - "react-hot-loader": "^1.0.7", - "sass-loader": "^1.0.1", - "style-loader": "~0.8.0", - "url-loader": "0.5.5", - "webpack": "~1.10.0", - "webpack-dev-server": "~1.10.0" - } -} diff --git a/demo/src/components/main.js b/demo/src/components/main.js deleted file mode 100644 index e3a4a76..0000000 --- a/demo/src/components/main.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict'; - -var MetricsUiApp = require('./MetricsUiApp'); -var GettingStarted = require('./GettingStarted'); -var ProjectList = require('./ProjectList'); -var NewProject = require('./NewProject'); -var Login = require('./Login'); -var Register = require('./Register'); -var Terms = require('./Terms'); -var React = require('react'); -import Report from './Report'; -import Router from 'react-router'; - -let { - DefaultRoute, - Route, -} = Router; - -var content = document.getElementById('content'); - -var Routes = ( - - - - - - - - - - -); - -Router.run(Routes, function (Handler) { - React.render(, content); -}); diff --git a/demo/src/libs/bemRouterLink.js b/demo/src/libs/bemRouterLink.js deleted file mode 100644 index 8593a16..0000000 --- a/demo/src/libs/bemRouterLink.js +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react/addons'; -import Router from 'react-router'; -import assign from 'react/lib/Object.assign'; -import bem from '../libs/react-create-bem-element'; - -/* -the purpose of this module is to allow you to pass the same -parameters you pass to and the same class parameters -you pass to bem(). - -Router.Link example: - - - Report 3 - - -With this: - - var SampleReport = bemRouterLink('sample-report') - - Report 3 - -*/ - -export default function(baseKls) { - var El = bem(baseKls, ''); - return React.createClass({ - mixins: [ - Router.Navigation, - ], - componentWillMount () { - var props = assign({}, this.props); - if (props.mTo) { - props.m = props.to = props.mTo; - } - if (!props.href) { - props.href = this.makeHref(props.to, props.params, props.query); - } - delete props.to; - delete props.mTo; - delete props.params; - delete props.query; - this._Props = props; - }, - render () { - return ; - } - }); -} diff --git a/demo/src/mixins/requireLogins.js b/demo/src/mixins/requireLogins.js deleted file mode 100644 index 2b1427f..0000000 --- a/demo/src/mixins/requireLogins.js +++ /dev/null @@ -1,33 +0,0 @@ -import sessionStore from '../stores/session'; - -export var [requireLoggedInMixin, requireNotLoggedInMixin] = (function(){ - var requireLoginValue = function(loginBool) { - return function authMixin({failTo, failToParams}) { - return { - componentDidMount () { - this._unsubscribe = sessionStore.listen(this._checkRequireLoginValue); - }, - componentWillUnmount () { - this._unsubscribe(); - }, - _checkRequireLoginValue (sessionState) { - console.log('requiring that login is ', loginBool, sessionState); - if (sessionState.loggedIn !== loginBool) { - this.transitionTo(failTo, failToParams); - } - }, - statics: { - willTransitionTo: function (transition, o1, o2, cb) { - if (loginBool && sessionStore.state.loggedIn === false) { - transition.redirect(failTo, failToParams); - } else if (!loginBool && sessionStore.state.loggedIn) { - transition.redirect(failTo, failToParams); - } - cb(); - } - }, - }; - }; - }; - return [requireLoginValue(true), requireLoginValue(false)]; -})(); diff --git a/demo/test/.jshintrc b/demo/test/.jshintrc deleted file mode 100644 index baa5704..0000000 --- a/demo/test/.jshintrc +++ /dev/null @@ -1,40 +0,0 @@ -{ - "node": true, - "browser": true, - "esnext": true, - "bitwise": true, - "camelcase": false, - "curly": true, - "eqeqeq": true, - "immed": true, - "indent": 2, - "latedef": true, - "newcap": true, - "noarg": true, - "quotmark": "false", - "regexp": true, - "undef": true, - "unused": false, - "strict": true, - "trailing": true, - "smarttabs": true, - "white": true, - "newcap": false, - "globals": { - "after": false, - "afterEach": false, - "react": false, - "before": false, - "beforeEach": false, - "browser": false, - "describe": false, - "expect": false, - "inject": false, - "it": false, - "spyOn": false, - "jasmine": false, - "spyOnConstructor": false, - "React": true - } -} - diff --git a/demo/test/helpers/createComponent.js b/demo/test/helpers/createComponent.js deleted file mode 100644 index 8a0c1da..0000000 --- a/demo/test/helpers/createComponent.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Function to get the shallow output for a given component - * As we are using phantom.js, we also need to include the fn.proto.bind shim! - * - * @see http://simonsmith.io/unit-testing-react-components-without-a-dom/ - * @author somonsmith - */ - -// Add missing methods to phantom.js -import './pack/phantomjs-shims'; - -import React from 'react/addons'; -const TestUtils = React.addons.TestUtils; - -/** - * Get the shallow rendered component - * - * @param {Object} component The component to return the output for - * @param {Object} props [optional] The components properties - * @param {Mixed} ...children [optional] List of children - * @return {Object} Shallow rendered output - */ -export default function createComponent(component, props = {}, ...children) { - const shallowRenderer = TestUtils.createRenderer(); - shallowRenderer.render(React.createElement(component, props, children.length > 1 ? children : children[0])); - return shallowRenderer.getRenderOutput(); -} diff --git a/demo/test/helpers/pack/phantomjs-shims.js b/demo/test/helpers/pack/phantomjs-shims.js deleted file mode 100644 index 7da307d..0000000 --- a/demo/test/helpers/pack/phantomjs-shims.js +++ /dev/null @@ -1,34 +0,0 @@ -(function() { - -var Ap = Array.prototype; -var slice = Ap.slice; -var Fp = Function.prototype; - -if (!Fp.bind) { - // PhantomJS doesn't support Function.prototype.bind natively, so - // polyfill it whenever this module is required. - Fp.bind = function(context) { - var func = this; - var args = slice.call(arguments, 1); - - function bound() { - var invokedAsConstructor = func.prototype && (this instanceof func); - return func.apply( - // Ignore the context parameter when invoking the bound function - // as a constructor. Note that this includes not only constructor - // invocations using the new keyword but also calls to base class - // constructors such as BaseClass.call(this, ...) or super(...). - !invokedAsConstructor && context || this, - args.concat(slice.call(arguments)) - ); - } - - // The bound function must share the .prototype of the unbound - // function so that any object created by one constructor will count - // as an instance of both constructors. - bound.prototype = func.prototype; - - return bound; - }; -} -})(); diff --git a/demo/test/helpers/react/addons.js b/demo/test/helpers/react/addons.js deleted file mode 100755 index 0bf44fe..0000000 --- a/demo/test/helpers/react/addons.js +++ /dev/null @@ -1,16336 +0,0 @@ -/** - * React (with addons) v0.9.0-alpha - */ -!function(e){"object"==typeof exports?module.exports=e():"function"==typeof define&&define.amd?define(e):"undefined"!=typeof window?window.React=e():"undefined"!=typeof global?global.React=e():"undefined"!=typeof self&&(self.React=e())}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o -1; -} - -var CSSCore = { - - /** - * Adds the class passed in to the element if it doesn't already have it. - * - * @param {DOMElement} element the element to set the class on - * @param {string} className the CSS className - * @return {DOMElement} the element passed in - */ - addClass: function(element, className) { - ("production" !== "development" ? invariant( - !/\s/.test(className), - 'CSSCore.addClass takes only a single class name. "%s" contains ' + - 'multiple classes.', className - ) : invariant(!/\s/.test(className))); - - if (className) { - if (element.classList) { - element.classList.add(className); - } else if (!hasClass(element, className)) { - element.className = element.className + ' ' + className; - } - } - return element; - }, - - /** - * Removes the class passed in from the element - * - * @param {DOMElement} element the element to set the class on - * @param {string} className the CSS className - * @return {DOMElement} the element passed in - */ - removeClass: function(element, className) { - ("production" !== "development" ? invariant( - !/\s/.test(className), - 'CSSCore.removeClass takes only a single class name. "%s" contains ' + - 'multiple classes.', className - ) : invariant(!/\s/.test(className))); - - if (className) { - if (element.classList) { - element.classList.remove(className); - } else if (hasClass(element, className)) { - element.className = element.className - .replace(new RegExp('(^|\\s)' + className + '(?:\\s|$)', 'g'), '$1') - .replace(/\s+/g, ' ') // multiple spaces to one - .replace(/^\s*|\s*$/g, ''); // trim the ends - } - } - return element; - }, - - /** - * Helper to add or remove a class from an element based on a condition. - * - * @param {DOMElement} element the element to set the class on - * @param {string} className the CSS className - * @param {*} bool condition to whether to add or remove the class - * @return {DOMElement} the element passed in - */ - conditionClass: function(element, className, bool) { - return (bool ? CSSCore.addClass : CSSCore.removeClass)(element, className); - } -}; - -module.exports = CSSCore; - -},{"./invariant":113}],3:[function(require,module,exports){ -/** - * Copyright 2013 Facebook, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * @providesModule CSSProperty - */ - -"use strict"; - -/** - * CSS properties which accept numbers but are not in units of "px". - */ -var isUnitlessNumber = { - fillOpacity: true, - fontWeight: true, - lineHeight: true, - opacity: true, - orphans: true, - zIndex: true, - zoom: true -}; - -/** - * Most style properties can be unset by doing .style[prop] = '' but IE8 - * doesn't like doing that with shorthand properties so for the properties that - * IE8 breaks on, which are listed here, we instead unset each of the - * individual properties. See http://bugs.jquery.com/ticket/12385. - * The 4-value 'clock' properties like margin, padding, border-width seem to - * behave without any problems. Curiously, list-style works too without any - * special prodding. - */ -var shorthandPropertyExpansions = { - background: { - backgroundImage: true, - backgroundPosition: true, - backgroundRepeat: true, - backgroundColor: true - }, - border: { - borderWidth: true, - borderStyle: true, - borderColor: true - }, - borderBottom: { - borderBottomWidth: true, - borderBottomStyle: true, - borderBottomColor: true - }, - borderLeft: { - borderLeftWidth: true, - borderLeftStyle: true, - borderLeftColor: true - }, - borderRight: { - borderRightWidth: true, - borderRightStyle: true, - borderRightColor: true - }, - borderTop: { - borderTopWidth: true, - borderTopStyle: true, - borderTopColor: true - }, - font: { - fontStyle: true, - fontVariant: true, - fontWeight: true, - fontSize: true, - lineHeight: true, - fontFamily: true - } -}; - -var CSSProperty = { - isUnitlessNumber: isUnitlessNumber, - shorthandPropertyExpansions: shorthandPropertyExpansions -}; - -module.exports = CSSProperty; - -},{}],4:[function(require,module,exports){ -/** - * Copyright 2013 Facebook, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * @providesModule CSSPropertyOperations - * @typechecks static-only - */ - -"use strict"; - -var CSSProperty = require("./CSSProperty"); - -var dangerousStyleValue = require("./dangerousStyleValue"); -var escapeTextForBrowser = require("./escapeTextForBrowser"); -var hyphenate = require("./hyphenate"); -var memoizeStringOnly = require("./memoizeStringOnly"); - -var processStyleName = memoizeStringOnly(function(styleName) { - return escapeTextForBrowser(hyphenate(styleName)); -}); - -/** - * Operations for dealing with CSS properties. - */ -var CSSPropertyOperations = { - - /** - * Serializes a mapping of style properties for use as inline styles: - * - * > createMarkupForStyles({width: '200px', height: 0}) - * "width:200px;height:0;" - * - * Undefined values are ignored so that declarative programming is easier. - * - * @param {object} styles - * @return {?string} - */ - createMarkupForStyles: function(styles) { - var serialized = ''; - for (var styleName in styles) { - if (!styles.hasOwnProperty(styleName)) { - continue; - } - var styleValue = styles[styleName]; - if (styleValue != null) { - serialized += processStyleName(styleName) + ':'; - serialized += dangerousStyleValue(styleName, styleValue) + ';'; - } - } - return serialized || null; - }, - - /** - * Sets the value for multiple styles on a node. If a value is specified as - * '' (empty string), the corresponding style property will be unset. - * - * @param {DOMElement} node - * @param {object} styles - */ - setValueForStyles: function(node, styles) { - var style = node.style; - for (var styleName in styles) { - if (!styles.hasOwnProperty(styleName)) { - continue; - } - var styleValue = dangerousStyleValue(styleName, styles[styleName]); - if (styleValue) { - style[styleName] = styleValue; - } else { - var expansion = CSSProperty.shorthandPropertyExpansions[styleName]; - if (expansion) { - // Shorthand property that IE8 won't like unsetting, so unset each - // component to placate it - for (var individualStyleName in expansion) { - style[individualStyleName] = ''; - } - } else { - style[styleName] = ''; - } - } - } - } - -}; - -module.exports = CSSPropertyOperations; - -},{"./CSSProperty":3,"./dangerousStyleValue":97,"./escapeTextForBrowser":99,"./hyphenate":112,"./memoizeStringOnly":121}],5:[function(require,module,exports){ -/** - * Copyright 2013 Facebook, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * @providesModule CallbackRegistry - * @typechecks static-only - */ - -"use strict"; - -var listenerBank = {}; - -/** - * Stores "listeners" by `registrationName`/`id`. There should be at most one - * "listener" per `registrationName`/`id` in the `listenerBank`. - * - * Access listeners via `listenerBank[registrationName][id]`. - * - * @class CallbackRegistry - * @internal - */ -var CallbackRegistry = { - - /** - * Stores `listener` at `listenerBank[registrationName][id]`. Is idempotent. - * - * @param {string} id ID of the DOM element. - * @param {string} registrationName Name of listener (e.g. `onClick`). - * @param {?function} listener The callback to store. - */ - putListener: function(id, registrationName, listener) { - var bankForRegistrationName = - listenerBank[registrationName] || (listenerBank[registrationName] = {}); - bankForRegistrationName[id] = listener; - }, - - /** - * @param {string} id ID of the DOM element. - * @param {string} registrationName Name of listener (e.g. `onClick`). - * @return {?function} The stored callback. - */ - getListener: function(id, registrationName) { - var bankForRegistrationName = listenerBank[registrationName]; - return bankForRegistrationName && bankForRegistrationName[id]; - }, - - /** - * Deletes a listener from the registration bank. - * - * @param {string} id ID of the DOM element. - * @param {string} registrationName Name of listener (e.g. `onClick`). - */ - deleteListener: function(id, registrationName) { - var bankForRegistrationName = listenerBank[registrationName]; - if (bankForRegistrationName) { - delete bankForRegistrationName[id]; - } - }, - - /** - * Deletes all listeners for the DOM element with the supplied ID. - * - * @param {string} id ID of the DOM element. - */ - deleteAllListeners: function(id) { - for (var registrationName in listenerBank) { - delete listenerBank[registrationName][id]; - } - }, - - /** - * This is needed for tests only. Do not use! - */ - __purge: function() { - listenerBank = {}; - } - -}; - -module.exports = CallbackRegistry; - -},{}],6:[function(require,module,exports){ -/** - * Copyright 2013 Facebook, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * @providesModule ChangeEventPlugin - */ - -"use strict"; - -var EventConstants = require("./EventConstants"); -var EventPluginHub = require("./EventPluginHub"); -var EventPropagators = require("./EventPropagators"); -var ExecutionEnvironment = require("./ExecutionEnvironment"); -var SyntheticEvent = require("./SyntheticEvent"); - -var isEventSupported = require("./isEventSupported"); -var isTextInputElement = require("./isTextInputElement"); -var keyOf = require("./keyOf"); - -var topLevelTypes = EventConstants.topLevelTypes; - -var eventTypes = { - change: { - phasedRegistrationNames: { - bubbled: keyOf({onChange: null}), - captured: keyOf({onChangeCapture: null}) - } - } -}; - -/** - * For IE shims - */ -var activeElement = null; -var activeElementID = null; -var activeElementValue = null; -var activeElementValueProp = null; - -/** - * SECTION: handle `change` event - */ -function shouldUseChangeEvent(elem) { - return ( - elem.nodeName === 'SELECT' || - (elem.nodeName === 'INPUT' && elem.type === 'file') - ); -} - -var doesChangeEventBubble = false; -if (ExecutionEnvironment.canUseDOM) { - // See `handleChange` comment below - doesChangeEventBubble = isEventSupported('change') && ( - !('documentMode' in document) || document.documentMode > 8 - ); -} - -function manualDispatchChangeEvent(nativeEvent) { - var event = SyntheticEvent.getPooled( - eventTypes.change, - activeElementID, - nativeEvent - ); - EventPropagators.accumulateTwoPhaseDispatches(event); - - // If change bubbled, we'd just bind to it like all the other events - // and have it go through ReactEventTopLevelCallback. Since it doesn't, we - // manually listen for the change event and so we have to enqueue and - // process the abstract event manually. - EventPluginHub.enqueueEvents(event); - EventPluginHub.processEventQueue(); -} - -function startWatchingForChangeEventIE8(target, targetID) { - activeElement = target; - activeElementID = targetID; - activeElement.attachEvent('onchange', manualDispatchChangeEvent); -} - -function stopWatchingForChangeEventIE8() { - if (!activeElement) { - return; - } - activeElement.detachEvent('onchange', manualDispatchChangeEvent); - activeElement = null; - activeElementID = null; -} - -function getTargetIDForChangeEvent( - topLevelType, - topLevelTarget, - topLevelTargetID) { - if (topLevelType === topLevelTypes.topChange) { - return topLevelTargetID; - } -} -function handleEventsForChangeEventIE8( - topLevelType, - topLevelTarget, - topLevelTargetID) { - if (topLevelType === topLevelTypes.topFocus) { - // stopWatching() should be a noop here but we call it just in case we - // missed a blur event somehow. - stopWatchingForChangeEventIE8(); - startWatchingForChangeEventIE8(topLevelTarget, topLevelTargetID); - } else if (topLevelType === topLevelTypes.topBlur) { - stopWatchingForChangeEventIE8(); - } -} - - -/** - * SECTION: handle `input` event - */ -var isInputEventSupported = false; -if (ExecutionEnvironment.canUseDOM) { - // IE9 claims to support the input event but fails to trigger it when - // deleting text, so we ignore its input events - isInputEventSupported = isEventSupported('input') && ( - !('documentMode' in document) || document.documentMode > 9 - ); -} - -/** - * (For old IE.) Replacement getter/setter for the `value` property that gets - * set on the active element. - */ -var newValueProp = { - get: function() { - return activeElementValueProp.get.call(this); - }, - set: function(val) { - // Cast to a string so we can do equality checks. - activeElementValue = '' + val; - activeElementValueProp.set.call(this, val); - } -}; - -/** - * (For old IE.) Starts tracking propertychange events on the passed-in element - * and override the value property so that we can distinguish user events from - * value changes in JS. - */ -function startWatchingForValueChange(target, targetID) { - activeElement = target; - activeElementID = targetID; - activeElementValue = target.value; - activeElementValueProp = Object.getOwnPropertyDescriptor( - target.constructor.prototype, - 'value' - ); - - Object.defineProperty(activeElement, 'value', newValueProp); - activeElement.attachEvent('onpropertychange', handlePropertyChange); -} - -/** - * (For old IE.) Removes the event listeners from the currently-tracked element, - * if any exists. - */ -function stopWatchingForValueChange() { - if (!activeElement) { - return; - } - - // delete restores the original property definition - delete activeElement.value; - activeElement.detachEvent('onpropertychange', handlePropertyChange); - - activeElement = null; - activeElementID = null; - activeElementValue = null; - activeElementValueProp = null; -} - -/** - * (For old IE.) Handles a propertychange event, sending a `change` event if - * the value of the active element has changed. - */ -function handlePropertyChange(nativeEvent) { - if (nativeEvent.propertyName !== 'value') { - return; - } - var value = nativeEvent.srcElement.value; - if (value === activeElementValue) { - return; - } - activeElementValue = value; - - manualDispatchChangeEvent(nativeEvent); -} - -/** - * If a `change` event should be fired, returns the target's ID. - */ -function getTargetIDForInputEvent( - topLevelType, - topLevelTarget, - topLevelTargetID) { - if (topLevelType === topLevelTypes.topInput) { - // In modern browsers (i.e., not IE8 or IE9), the input event is exactly - // what we want so fall through here and trigger an abstract event - return topLevelTargetID; - } -} - -// For IE8 and IE9. -function handleEventsForInputEventIE( - topLevelType, - topLevelTarget, - topLevelTargetID) { - if (topLevelType === topLevelTypes.topFocus) { - // In IE8, we can capture almost all .value changes by adding a - // propertychange handler and looking for events with propertyName - // equal to 'value' - // In IE9, propertychange fires for most input events but is buggy and - // doesn't fire when text is deleted, but conveniently, selectionchange - // appears to fire in all of the remaining cases so we catch those and - // forward the event if the value has changed - // In either case, we don't want to call the event handler if the value - // is changed from JS so we redefine a setter for `.value` that updates - // our activeElementValue variable, allowing us to ignore those changes - // - // stopWatching() should be a noop here but we call it just in case we - // missed a blur event somehow. - stopWatchingForValueChange(); - startWatchingForValueChange(topLevelTarget, topLevelTargetID); - } else if (topLevelType === topLevelTypes.topBlur) { - stopWatchingForValueChange(); - } -} - -// For IE8 and IE9. -function getTargetIDForInputEventIE( - topLevelType, - topLevelTarget, - topLevelTargetID) { - if (topLevelType === topLevelTypes.topSelectionChange || - topLevelType === topLevelTypes.topKeyUp || - topLevelType === topLevelTypes.topKeyDown) { - // On the selectionchange event, the target is just document which isn't - // helpful for us so just check activeElement instead. - // - // 99% of the time, keydown and keyup aren't necessary. IE8 fails to fire - // propertychange on the first input event after setting `value` from a - // script and fires only keydown, keypress, keyup. Catching keyup usually - // gets it and catching keydown lets us fire an event for the first - // keystroke if user does a key repeat (it'll be a little delayed: right - // before the second keystroke). Other input methods (e.g., paste) seem to - // fire selectionchange normally. - if (activeElement && activeElement.value !== activeElementValue) { - activeElementValue = activeElement.value; - return activeElementID; - } - } -} - - -/** - * SECTION: handle `click` event - */ -function shouldUseClickEvent(elem) { - // Use the `click` event to detect changes to checkbox and radio inputs. - // This approach works across all browsers, whereas `change` does not fire - // until `blur` in IE8. - return ( - elem.nodeName === 'INPUT' && - (elem.type === 'checkbox' || elem.type === 'radio') - ); -} - -function getTargetIDForClickEvent( - topLevelType, - topLevelTarget, - topLevelTargetID) { - if (topLevelType === topLevelTypes.topClick) { - return topLevelTargetID; - } -} - -/** - * This plugin creates an `onChange` event that normalizes change events - * across form elements. This event fires at a time when it's possible to - * change the element's value without seeing a flicker. - * - * Supported elements are: - * - input (see `isTextInputElement`) - * - textarea - * - select - */ -var ChangeEventPlugin = { - - eventTypes: eventTypes, - - /** - * @param {string} topLevelType Record from `EventConstants`. - * @param {DOMEventTarget} topLevelTarget The listening component root node. - * @param {string} topLevelTargetID ID of `topLevelTarget`. - * @param {object} nativeEvent Native browser event. - * @return {*} An accumulation of synthetic events. - * @see {EventPluginHub.extractEvents} - */ - extractEvents: function( - topLevelType, - topLevelTarget, - topLevelTargetID, - nativeEvent) { - - var getTargetIDFunc, handleEventFunc; - if (shouldUseChangeEvent(topLevelTarget)) { - if (doesChangeEventBubble) { - getTargetIDFunc = getTargetIDForChangeEvent; - } else { - handleEventFunc = handleEventsForChangeEventIE8; - } - } else if (isTextInputElement(topLevelTarget)) { - if (isInputEventSupported) { - getTargetIDFunc = getTargetIDForInputEvent; - } else { - getTargetIDFunc = getTargetIDForInputEventIE; - handleEventFunc = handleEventsForInputEventIE; - } - } else if (shouldUseClickEvent(topLevelTarget)) { - getTargetIDFunc = getTargetIDForClickEvent; - } - - if (getTargetIDFunc) { - var targetID = getTargetIDFunc( - topLevelType, - topLevelTarget, - topLevelTargetID - ); - if (targetID) { - var event = SyntheticEvent.getPooled( - eventTypes.change, - targetID, - nativeEvent - ); - EventPropagators.accumulateTwoPhaseDispatches(event); - return event; - } - } - - if (handleEventFunc) { - handleEventFunc( - topLevelType, - topLevelTarget, - topLevelTargetID - ); - } - } - -}; - -module.exports = ChangeEventPlugin; - -},{"./EventConstants":15,"./EventPluginHub":17,"./EventPropagators":20,"./ExecutionEnvironment":21,"./SyntheticEvent":80,"./isEventSupported":114,"./isTextInputElement":116,"./keyOf":120}],7:[function(require,module,exports){ -/** - * Copyright 2013 Facebook, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * @providesModule CompositionEventPlugin - * @typechecks static-only - */ - -"use strict"; - -var EventConstants = require("./EventConstants"); -var EventPropagators = require("./EventPropagators"); -var ExecutionEnvironment = require("./ExecutionEnvironment"); -var ReactInputSelection = require("./ReactInputSelection"); -var SyntheticCompositionEvent = require("./SyntheticCompositionEvent"); - -var getTextContentAccessor = require("./getTextContentAccessor"); -var keyOf = require("./keyOf"); - -var END_KEYCODES = [9, 13, 27, 32]; // Tab, Return, Esc, Space -var START_KEYCODE = 229; - -var useCompositionEvent = ExecutionEnvironment.canUseDOM && - 'CompositionEvent' in window; -var topLevelTypes = EventConstants.topLevelTypes; -var currentComposition = null; - -// Events and their corresponding property names. -var eventTypes = { - compositionEnd: { - phasedRegistrationNames: { - bubbled: keyOf({onCompositionEnd: null}), - captured: keyOf({onCompositionEndCapture: null}) - } - }, - compositionStart: { - phasedRegistrationNames: { - bubbled: keyOf({onCompositionStart: null}), - captured: keyOf({onCompositionStartCapture: null}) - } - }, - compositionUpdate: { - phasedRegistrationNames: { - bubbled: keyOf({onCompositionUpdate: null}), - captured: keyOf({onCompositionUpdateCapture: null}) - } - } -}; - -/** - * Translate native top level events into event types. - * - * @param {string} topLevelType - * @return {object} - */ -function getCompositionEventType(topLevelType) { - switch (topLevelType) { - case topLevelTypes.topCompositionStart: - return eventTypes.compositionStart; - case topLevelTypes.topCompositionEnd: - return eventTypes.compositionEnd; - case topLevelTypes.topCompositionUpdate: - return eventTypes.compositionUpdate; - } -} - -/** - * Does our fallback best-guess model think this event signifies that - * composition has begun? - * - * @param {string} topLevelType - * @param {object} nativeEvent - * @return {boolean} - */ -function isFallbackStart(topLevelType, nativeEvent) { - return ( - topLevelType === topLevelTypes.topKeyDown && - nativeEvent.keyCode === START_KEYCODE - ); -} - -/** - * Does our fallback mode think that this event is the end of composition? - * - * @param {string} topLevelType - * @param {object} nativeEvent - * @return {boolean} - */ -function isFallbackEnd(topLevelType, nativeEvent) { - switch (topLevelType) { - case topLevelTypes.topKeyUp: - // Command keys insert or clear IME input. - return (END_KEYCODES.indexOf(nativeEvent.keyCode) !== -1); - case topLevelTypes.topKeyDown: - // Expect IME keyCode on each keydown. If we get any other - // code we must have exited earlier. - return (nativeEvent.keyCode !== START_KEYCODE); - case topLevelTypes.topKeyPress: - case topLevelTypes.topMouseDown: - case topLevelTypes.topBlur: - // Events are not possible without cancelling IME. - return true; - default: - return false; - } -} - -/** - * Helper class stores information about selection and document state - * so we can figure out what changed at a later date. - * - * @param {DOMEventTarget} root - */ -function FallbackCompositionState(root) { - this.root = root; - this.startSelection = ReactInputSelection.getSelection(root); - this.startValue = this.getText(); -} - -/** - * Get current text of input. - * - * @return {string} - */ -FallbackCompositionState.prototype.getText = function() { - return this.root.value || this.root[getTextContentAccessor()]; -}; - -/** - * Text that has changed since the start of composition. - * - * @return {string} - */ -FallbackCompositionState.prototype.getData = function() { - var endValue = this.getText(); - var prefixLength = this.startSelection.start; - var suffixLength = this.startValue.length - this.startSelection.end; - - return endValue.substr( - prefixLength, - endValue.length - suffixLength - prefixLength - ); -}; - -/** - * This plugin creates `onCompositionStart`, `onCompositionUpdate` and - * `onCompositionEnd` events on inputs, textareas and contentEditable - * nodes. - */ -var CompositionEventPlugin = { - - eventTypes: eventTypes, - - /** - * @param {string} topLevelType Record from `EventConstants`. - * @param {DOMEventTarget} topLevelTarget The listening component root node. - * @param {string} topLevelTargetID ID of `topLevelTarget`. - * @param {object} nativeEvent Native browser event. - * @return {*} An accumulation of synthetic events. - * @see {EventPluginHub.extractEvents} - */ - extractEvents: function( - topLevelType, - topLevelTarget, - topLevelTargetID, - nativeEvent) { - - var eventType; - var data; - - if (useCompositionEvent) { - eventType = getCompositionEventType(topLevelType); - } else if (!currentComposition) { - if (isFallbackStart(topLevelType, nativeEvent)) { - eventType = eventTypes.start; - currentComposition = new FallbackCompositionState(topLevelTarget); - } - } else if (isFallbackEnd(topLevelType, nativeEvent)) { - eventType = eventTypes.compositionEnd; - data = currentComposition.getData(); - currentComposition = null; - } - - if (eventType) { - var event = SyntheticCompositionEvent.getPooled( - eventType, - topLevelTargetID, - nativeEvent - ); - if (data) { - // Inject data generated from fallback path into the synthetic event. - // This matches the property of native CompositionEventInterface. - event.data = data; - } - EventPropagators.accumulateTwoPhaseDispatches(event); - return event; - } - } -}; - -module.exports = CompositionEventPlugin; - -},{"./EventConstants":15,"./EventPropagators":20,"./ExecutionEnvironment":21,"./ReactInputSelection":51,"./SyntheticCompositionEvent":79,"./getTextContentAccessor":110,"./keyOf":120}],8:[function(require,module,exports){ -/** - * Copyright 2013 Facebook, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * @providesModule DOMChildrenOperations - * @typechecks static-only - */ - -"use strict"; - -var Danger = require("./Danger"); -var ReactMultiChildUpdateTypes = require("./ReactMultiChildUpdateTypes"); - -var getTextContentAccessor = require("./getTextContentAccessor"); - -/** - * The DOM property to use when setting text content. - * - * @type {string} - * @private - */ -var textContentAccessor = getTextContentAccessor() || 'NA'; - -/** - * Inserts `childNode` as a child of `parentNode` at the `index`. - * - * @param {DOMElement} parentNode Parent node in which to insert. - * @param {DOMElement} childNode Child node to insert. - * @param {number} index Index at which to insert the child. - * @internal - */ -function insertChildAt(parentNode, childNode, index) { - var childNodes = parentNode.childNodes; - if (childNodes[index] === childNode) { - return; - } - // If `childNode` is already a child of `parentNode`, remove it so that - // computing `childNodes[index]` takes into account the removal. - if (childNode.parentNode === parentNode) { - parentNode.removeChild(childNode); - } - if (index >= childNodes.length) { - parentNode.appendChild(childNode); - } else { - parentNode.insertBefore(childNode, childNodes[index]); - } -} - -/** - * Operations for updating with DOM children. - */ -var DOMChildrenOperations = { - - dangerouslyReplaceNodeWithMarkup: Danger.dangerouslyReplaceNodeWithMarkup, - - /** - * Updates a component's children by processing a series of updates. The - * update configurations are each expected to have a `parentNode` property. - * - * @param {array} updates List of update configurations. - * @param {array} markupList List of markup strings. - * @internal - */ - processUpdates: function(updates, markupList) { - var update; - // Mapping from parent IDs to initial child orderings. - var initialChildren = null; - // List of children that will be moved or removed. - var updatedChildren = null; - - for (var i = 0; update = updates[i]; i++) { - if (update.type === ReactMultiChildUpdateTypes.MOVE_EXISTING || - update.type === ReactMultiChildUpdateTypes.REMOVE_NODE) { - var updatedIndex = update.fromIndex; - var updatedChild = update.parentNode.childNodes[updatedIndex]; - var parentID = update.parentID; - - initialChildren = initialChildren || {}; - initialChildren[parentID] = initialChildren[parentID] || []; - initialChildren[parentID][updatedIndex] = updatedChild; - - updatedChildren = updatedChildren || []; - updatedChildren.push(updatedChild); - } - } - - var renderedMarkup = Danger.dangerouslyRenderMarkup(markupList); - - // Remove updated children first so that `toIndex` is consistent. - if (updatedChildren) { - for (var j = 0; j < updatedChildren.length; j++) { - updatedChildren[j].parentNode.removeChild(updatedChildren[j]); - } - } - - for (var k = 0; update = updates[k]; k++) { - switch (update.type) { - case ReactMultiChildUpdateTypes.INSERT_MARKUP: - insertChildAt( - update.parentNode, - renderedMarkup[update.markupIndex], - update.toIndex - ); - break; - case ReactMultiChildUpdateTypes.MOVE_EXISTING: - insertChildAt( - update.parentNode, - initialChildren[update.parentID][update.fromIndex], - update.toIndex - ); - break; - case ReactMultiChildUpdateTypes.TEXT_CONTENT: - update.parentNode[textContentAccessor] = update.textContent; - break; - case ReactMultiChildUpdateTypes.REMOVE_NODE: - // Already removed by the for-loop above. - break; - } - } - } - -}; - -module.exports = DOMChildrenOperations; - -},{"./Danger":11,"./ReactMultiChildUpdateTypes":58,"./getTextContentAccessor":110}],9:[function(require,module,exports){ -/** - * Copyright 2013 Facebook, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * @providesModule DOMProperty - * @typechecks static-only - */ - -/*jslint bitwise: true */ - -"use strict"; - -var invariant = require("./invariant"); - -var DOMPropertyInjection = { - /** - * Mapping from normalized, camelcased property names to a configuration that - * specifies how the associated DOM property should be accessed or rendered. - */ - MUST_USE_ATTRIBUTE: 0x1, - MUST_USE_PROPERTY: 0x2, - HAS_SIDE_EFFECTS: 0x4, - HAS_BOOLEAN_VALUE: 0x8, - HAS_POSITIVE_NUMERIC_VALUE: 0x10, - - /** - * Inject some specialized knowledge about the DOM. This takes a config object - * with the following properties: - * - * isCustomAttribute: function that given an attribute name will return true - * if it can be inserted into the DOM verbatim. Useful for data-* or aria-* - * attributes where it's impossible to enumerate all of the possible - * attribute names, - * - * Properties: object mapping DOM property name to one of the - * DOMPropertyInjection constants or null. If your attribute isn't in here, - * it won't get written to the DOM. - * - * DOMAttributeNames: object mapping React attribute name to the DOM - * attribute name. Attribute names not specified use the **lowercase** - * normalized name. - * - * DOMPropertyNames: similar to DOMAttributeNames but for DOM properties. - * Property names not specified use the normalized name. - * - * DOMMutationMethods: Properties that require special mutation methods. If - * `value` is undefined, the mutation method should unset the property. - * - * @param {object} domPropertyConfig the config as described above. - */ - injectDOMPropertyConfig: function(domPropertyConfig) { - var Properties = domPropertyConfig.Properties || {}; - var DOMAttributeNames = domPropertyConfig.DOMAttributeNames || {}; - var DOMPropertyNames = domPropertyConfig.DOMPropertyNames || {}; - var DOMMutationMethods = domPropertyConfig.DOMMutationMethods || {}; - - if (domPropertyConfig.isCustomAttribute) { - DOMProperty._isCustomAttributeFunctions.push( - domPropertyConfig.isCustomAttribute - ); - } - - for (var propName in Properties) { - ("production" !== "development" ? invariant( - !DOMProperty.isStandardName[propName], - 'injectDOMPropertyConfig(...): You\'re trying to inject DOM property ' + - '\'%s\' which has already been injected. You may be accidentally ' + - 'injecting the same DOM property config twice, or you may be ' + - 'injecting two configs that have conflicting property names.', - propName - ) : invariant(!DOMProperty.isStandardName[propName])); - - DOMProperty.isStandardName[propName] = true; - - var lowerCased = propName.toLowerCase(); - DOMProperty.getPossibleStandardName[lowerCased] = propName; - - var attributeName = DOMAttributeNames[propName]; - if (attributeName) { - DOMProperty.getPossibleStandardName[attributeName] = propName; - } - - DOMProperty.getAttributeName[propName] = attributeName || lowerCased; - - DOMProperty.getPropertyName[propName] = - DOMPropertyNames[propName] || propName; - - var mutationMethod = DOMMutationMethods[propName]; - if (mutationMethod) { - DOMProperty.getMutationMethod[propName] = mutationMethod; - } - - var propConfig = Properties[propName]; - DOMProperty.mustUseAttribute[propName] = - propConfig & DOMPropertyInjection.MUST_USE_ATTRIBUTE; - DOMProperty.mustUseProperty[propName] = - propConfig & DOMPropertyInjection.MUST_USE_PROPERTY; - DOMProperty.hasSideEffects[propName] = - propConfig & DOMPropertyInjection.HAS_SIDE_EFFECTS; - DOMProperty.hasBooleanValue[propName] = - propConfig & DOMPropertyInjection.HAS_BOOLEAN_VALUE; - DOMProperty.hasPositiveNumericValue[propName] = - propConfig & DOMPropertyInjection.HAS_POSITIVE_NUMERIC_VALUE; - - ("production" !== "development" ? invariant( - !DOMProperty.mustUseAttribute[propName] || - !DOMProperty.mustUseProperty[propName], - 'DOMProperty: Cannot require using both attribute and property: %s', - propName - ) : invariant(!DOMProperty.mustUseAttribute[propName] || - !DOMProperty.mustUseProperty[propName])); - ("production" !== "development" ? invariant( - DOMProperty.mustUseProperty[propName] || - !DOMProperty.hasSideEffects[propName], - 'DOMProperty: Properties that have side effects must use property: %s', - propName - ) : invariant(DOMProperty.mustUseProperty[propName] || - !DOMProperty.hasSideEffects[propName])); - ("production" !== "development" ? invariant( - !DOMProperty.hasBooleanValue[propName] || - !DOMProperty.hasPositiveNumericValue[propName], - 'DOMProperty: Cannot have both boolean and positive numeric value: %s', - propName - ) : invariant(!DOMProperty.hasBooleanValue[propName] || - !DOMProperty.hasPositiveNumericValue[propName])); - } - } -}; -var defaultValueCache = {}; - -/** - * DOMProperty exports lookup objects that can be used like functions: - * - * > DOMProperty.isValid['id'] - * true - * > DOMProperty.isValid['foobar'] - * undefined - * - * Although this may be confusing, it performs better in general. - * - * @see http://jsperf.com/key-exists - * @see http://jsperf.com/key-missing - */ -var DOMProperty = { - - /** - * Checks whether a property name is a standard property. - * @type {Object} - */ - isStandardName: {}, - - /** - * Mapping from lowercase property names to the properly cased version, used - * to warn in the case of missing properties. - * @type {Object} - */ - getPossibleStandardName: {}, - - /** - * Mapping from normalized names to attribute names that differ. Attribute - * names are used when rendering markup or with `*Attribute()`. - * @type {Object} - */ - getAttributeName: {}, - - /** - * Mapping from normalized names to properties on DOM node instances. - * (This includes properties that mutate due to external factors.) - * @type {Object} - */ - getPropertyName: {}, - - /** - * Mapping from normalized names to mutation methods. This will only exist if - * mutation cannot be set simply by the property or `setAttribute()`. - * @type {Object} - */ - getMutationMethod: {}, - - /** - * Whether the property must be accessed and mutated as an object property. - * @type {Object} - */ - mustUseAttribute: {}, - - /** - * Whether the property must be accessed and mutated using `*Attribute()`. - * (This includes anything that fails ` in `.) - * @type {Object} - */ - mustUseProperty: {}, - - /** - * Whether or not setting a value causes side effects such as triggering - * resources to be loaded or text selection changes. We must ensure that - * the value is only set if it has changed. - * @type {Object} - */ - hasSideEffects: {}, - - /** - * Whether the property should be removed when set to a falsey value. - * @type {Object} - */ - hasBooleanValue: {}, - - /** - * Whether the property must be positive numeric or parse as a positive - * numeric and should be removed when set to a falsey value. - * @type {Object} - */ - hasPositiveNumericValue: {}, - - /** - * All of the isCustomAttribute() functions that have been injected. - */ - _isCustomAttributeFunctions: [], - - /** - * Checks whether a property name is a custom attribute. - * @method - */ - isCustomAttribute: function(attributeName) { - return DOMProperty._isCustomAttributeFunctions.some( - function(isCustomAttributeFn) { - return isCustomAttributeFn.call(null, attributeName); - } - ); - }, - - /** - * Returns the default property value for a DOM property (i.e., not an - * attribute). Most default values are '' or false, but not all. Worse yet, - * some (in particular, `type`) vary depending on the type of element. - * - * TODO: Is it better to grab all the possible properties when creating an - * element to avoid having to create the same element twice? - */ - getDefaultValueForProperty: function(nodeName, prop) { - var nodeDefaults = defaultValueCache[nodeName]; - var testElement; - if (!nodeDefaults) { - defaultValueCache[nodeName] = nodeDefaults = {}; - } - if (!(prop in nodeDefaults)) { - testElement = document.createElement(nodeName); - nodeDefaults[prop] = testElement[prop]; - } - return nodeDefaults[prop]; - }, - - injection: DOMPropertyInjection -}; - -module.exports = DOMProperty; - -},{"./invariant":113}],10:[function(require,module,exports){ -/** - * Copyright 2013 Facebook, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * @providesModule DOMPropertyOperations - * @typechecks static-only - */ - -"use strict"; - -var DOMProperty = require("./DOMProperty"); - -var escapeTextForBrowser = require("./escapeTextForBrowser"); -var memoizeStringOnly = require("./memoizeStringOnly"); - -function shouldIgnoreValue(name, value) { - return value == null || - DOMProperty.hasBooleanValue[name] && !value || - DOMProperty.hasPositiveNumericValue[name] && (isNaN(value) || value < 1); -} - -var processAttributeNameAndPrefix = memoizeStringOnly(function(name) { - return escapeTextForBrowser(name) + '="'; -}); - -if ("production" !== "development") { - var reactProps = { - children: true, - dangerouslySetInnerHTML: true, - key: true, - ref: true - }; - var warnedProperties = {}; - - var warnUnknownProperty = function(name) { - if (reactProps[name] || warnedProperties[name]) { - return; - } - - warnedProperties[name] = true; - var lowerCasedName = name.toLowerCase(); - - // data-* attributes should be lowercase; suggest the lowercase version - var standardName = DOMProperty.isCustomAttribute(lowerCasedName) ? - lowerCasedName : DOMProperty.getPossibleStandardName[lowerCasedName]; - - // For now, only warn when we have a suggested correction. This prevents - // logging too much when using transferPropsTo. - if (standardName != null) { - console.warn( - 'Unknown DOM property ' + name + '. Did you mean ' + standardName + '?' - ); - } - - }; -} - -/** - * Operations for dealing with DOM properties. - */ -var DOMPropertyOperations = { - - /** - * Creates markup for a property. - * - * @param {string} name - * @param {*} value - * @return {?string} Markup string, or null if the property was invalid. - */ - createMarkupForProperty: function(name, value) { - if (DOMProperty.isStandardName[name]) { - if (shouldIgnoreValue(name, value)) { - return ''; - } - var attributeName = DOMProperty.getAttributeName[name]; - return processAttributeNameAndPrefix(attributeName) + - escapeTextForBrowser(value) + '"'; - } else if (DOMProperty.isCustomAttribute(name)) { - if (value == null) { - return ''; - } - return processAttributeNameAndPrefix(name) + - escapeTextForBrowser(value) + '"'; - } else if ("production" !== "development") { - warnUnknownProperty(name); - } - return null; - }, - - /** - * Sets the value for a property on a node. - * - * @param {DOMElement} node - * @param {string} name - * @param {*} value - */ - setValueForProperty: function(node, name, value) { - if (DOMProperty.isStandardName[name]) { - var mutationMethod = DOMProperty.getMutationMethod[name]; - if (mutationMethod) { - mutationMethod(node, value); - } else if (shouldIgnoreValue(name, value)) { - this.deleteValueForProperty(node, name); - } else if (DOMProperty.mustUseAttribute[name]) { - node.setAttribute(DOMProperty.getAttributeName[name], '' + value); - } else { - var propName = DOMProperty.getPropertyName[name]; - if (!DOMProperty.hasSideEffects[name] || node[propName] !== value) { - node[propName] = value; - } - } - } else if (DOMProperty.isCustomAttribute(name)) { - if (value == null) { - node.removeAttribute(DOMProperty.getAttributeName[name]); - } else { - node.setAttribute(name, '' + value); - } - } else if ("production" !== "development") { - warnUnknownProperty(name); - } - }, - - /** - * Deletes the value for a property on a node. - * - * @param {DOMElement} node - * @param {string} name - */ - deleteValueForProperty: function(node, name) { - if (DOMProperty.isStandardName[name]) { - var mutationMethod = DOMProperty.getMutationMethod[name]; - if (mutationMethod) { - mutationMethod(node, undefined); - } else if (DOMProperty.mustUseAttribute[name]) { - node.removeAttribute(DOMProperty.getAttributeName[name]); - } else { - var propName = DOMProperty.getPropertyName[name]; - var defaultValue = DOMProperty.getDefaultValueForProperty( - node.nodeName, - name - ); - if (!DOMProperty.hasSideEffects[name] || - node[propName] !== defaultValue) { - node[propName] = defaultValue; - } - } - } else if (DOMProperty.isCustomAttribute(name)) { - node.removeAttribute(name); - } else if ("production" !== "development") { - warnUnknownProperty(name); - } - } - -}; - -module.exports = DOMPropertyOperations; - -},{"./DOMProperty":9,"./escapeTextForBrowser":99,"./memoizeStringOnly":121}],11:[function(require,module,exports){ -/** - * Copyright 2013 Facebook, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * @providesModule Danger - * @typechecks static-only - */ - -/*jslint evil: true, sub: true */ - -"use strict"; - -var ExecutionEnvironment = require("./ExecutionEnvironment"); - -var createNodesFromMarkup = require("./createNodesFromMarkup"); -var emptyFunction = require("./emptyFunction"); -var getMarkupWrap = require("./getMarkupWrap"); -var invariant = require("./invariant"); -var mutateHTMLNodeWithMarkup = require("./mutateHTMLNodeWithMarkup"); - -var OPEN_TAG_NAME_EXP = /^(<[^ \/>]+)/; -var RESULT_INDEX_ATTR = 'data-danger-index'; - -/** - * Extracts the `nodeName` from a string of markup. - * - * NOTE: Extracting the `nodeName` does not require a regular expression match - * because we make assumptions about React-generated markup (i.e. there are no - * spaces surrounding the opening tag and there is at least one attribute). - * - * @param {string} markup String of markup. - * @return {string} Node name of the supplied markup. - * @see http://jsperf.com/extract-nodename - */ -function getNodeName(markup) { - return markup.substring(1, markup.indexOf(' ')); -} - -var Danger = { - - /** - * Renders markup into an array of nodes. The markup is expected to render - * into a list of root nodes. Also, the length of `resultList` and - * `markupList` should be the same. - * - * @param {array} markupList List of markup strings to render. - * @return {array} List of rendered nodes. - * @internal - */ - dangerouslyRenderMarkup: function(markupList) { - ("production" !== "development" ? invariant( - ExecutionEnvironment.canUseDOM, - 'dangerouslyRenderMarkup(...): Cannot render markup in a Worker ' + - 'thread. This is likely a bug in the framework. Please report ' + - 'immediately.' - ) : invariant(ExecutionEnvironment.canUseDOM)); - var nodeName; - var markupByNodeName = {}; - // Group markup by `nodeName` if a wrap is necessary, else by '*'. - for (var i = 0; i < markupList.length; i++) { - ("production" !== "development" ? invariant( - markupList[i], - 'dangerouslyRenderMarkup(...): Missing markup.' - ) : invariant(markupList[i])); - nodeName = getNodeName(markupList[i]); - nodeName = getMarkupWrap(nodeName) ? nodeName : '*'; - markupByNodeName[nodeName] = markupByNodeName[nodeName] || []; - markupByNodeName[nodeName][i] = markupList[i]; - } - var resultList = []; - var resultListAssignmentCount = 0; - for (nodeName in markupByNodeName) { - if (!markupByNodeName.hasOwnProperty(nodeName)) { - continue; - } - var markupListByNodeName = markupByNodeName[nodeName]; - - // This for-in loop skips the holes of the sparse array. The order of - // iteration should follow the order of assignment, which happens to match - // numerical index order, but we don't rely on that. - for (var resultIndex in markupListByNodeName) { - if (markupListByNodeName.hasOwnProperty(resultIndex)) { - var markup = markupListByNodeName[resultIndex]; - - // Push the requested markup with an additional RESULT_INDEX_ATTR - // attribute. If the markup does not start with a < character, it - // will be discarded below (with an appropriate console.error). - markupListByNodeName[resultIndex] = markup.replace( - OPEN_TAG_NAME_EXP, - // This index will be parsed back out below. - '$1 ' + RESULT_INDEX_ATTR + '="' + resultIndex + '" ' - ); - } - } - - // Render each group of markup with similar wrapping `nodeName`. - var renderNodes = createNodesFromMarkup( - markupListByNodeName.join(''), - emptyFunction // Do nothing special with