├── example ├── img │ └── .gitignore ├── .gitattributes ├── tile.png ├── favicon.ico ├── tile-wide.png ├── css │ ├── home.css │ ├── main.css │ └── normalize.css ├── robots.txt ├── apple-touch-icon.png ├── .gitignore ├── .editorconfig ├── browserconfig.xml ├── crossdomain.xml ├── js │ ├── plugins.js │ ├── main.jsx │ ├── residue.js │ ├── example_settings.jsx │ └── bipyridine.js ├── webpack.config.js ├── index.html └── .htaccess ├── .npmignore ├── .eslintignore ├── .gitignore ├── doc └── viewer_screenshot.png ├── src ├── styles │ ├── nodes.scss │ └── links.scss ├── main.js ├── utils │ ├── mol_view_utils.js │ └── molecule_utils.js └── components │ ├── nodes.jsx │ ├── links.jsx │ └── molecule_2d.jsx ├── .babelrc ├── test ├── e2e │ ├── fixtures │ │ └── setup.js │ └── specs │ │ └── selection_spec.js └── unit │ └── utils │ ├── mol_view_utils_spec.js │ └── molecule_utils_spec.js ├── .eslintrc ├── scripts └── download_selenium.js ├── .travis.yml ├── webpack.config.js ├── nightwatch.conf.js ├── package.json ├── karma.conf.js ├── README.md └── LICENSE /example/img/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | example/ 3 | -------------------------------------------------------------------------------- /example/.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | example/js/residue.js 2 | example/js/bipyridine.js 3 | -------------------------------------------------------------------------------- /example/tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autodesk/molecule-2d-for-react/HEAD/example/tile.png -------------------------------------------------------------------------------- /example/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autodesk/molecule-2d-for-react/HEAD/example/favicon.ico -------------------------------------------------------------------------------- /example/tile-wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autodesk/molecule-2d-for-react/HEAD/example/tile-wide.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | npm-debug.log 4 | selenium-debug.log 5 | reports/ 6 | screenshots/ 7 | -------------------------------------------------------------------------------- /example/css/home.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | } 4 | 5 | .data { 6 | margin-left: 4rem; 7 | } 8 | -------------------------------------------------------------------------------- /doc/viewer_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autodesk/molecule-2d-for-react/HEAD/doc/viewer_screenshot.png -------------------------------------------------------------------------------- /example/robots.txt: -------------------------------------------------------------------------------- 1 | # www.robotstxt.org/ 2 | 3 | # Allow crawling of all content 4 | User-agent: * 5 | Disallow: 6 | -------------------------------------------------------------------------------- /example/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autodesk/molecule-2d-for-react/HEAD/example/apple-touch-icon.png -------------------------------------------------------------------------------- /src/styles/nodes.scss: -------------------------------------------------------------------------------- 1 | .node { 2 | text { 3 | font: 10px sans-serif; 4 | pointer-events: none; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "es2016", 5 | "es2017", 6 | "stage-2", 7 | "react" 8 | ] 9 | } 10 | 11 | -------------------------------------------------------------------------------- /test/e2e/fixtures/setup.js: -------------------------------------------------------------------------------- 1 | module.exports = function setup(browser) { 2 | browser.windowSize('current', 1700, 1100); 3 | 4 | return browser; 5 | }; 6 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Include your project-specific ignores in this file 2 | # Read about how to use .gitignore: https://help.github.com/articles/ignoring-files 3 | -------------------------------------------------------------------------------- /src/styles/links.scss: -------------------------------------------------------------------------------- 1 | .link { 2 | line { 3 | stroke: #696969; 4 | 5 | &.separator { 6 | stroke: #fff; 7 | stroke-width: 2px; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_size = 4 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "rules": { 4 | "react/no-unused-prop-types": 1, 5 | // These rules are violated by normal d3 usage 6 | "no-underscore-dangle": 1, 7 | "no-param-reassign": 1, 8 | "react/jsx-no-bind": 1, 9 | }, 10 | "parser": "babel-eslint", 11 | "globals": { 12 | "window": true, 13 | "document": true, 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /example/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /scripts/download_selenium.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const seleniumDownload = require('selenium-download'); 3 | 4 | const BINPATH = './node_modules/nightwatch/bin/'; 5 | 6 | /** 7 | * selenium-download does exactly what it's name suggests; 8 | * downloads (or updates) the version of Selenium (& chromedriver) 9 | * on your localhost where it will be used by Nightwatch. 10 | */ 11 | fs.stat(`${BINPATH}selenium.jar`, (err, stat) => { 12 | if (err || !stat || stat.size < 1) { 13 | seleniumDownload.ensure(BINPATH, (error) => { 14 | if (error) throw new Error(error); 15 | console.log('✔ Selenium & Chromedriver downloaded to:', BINPATH); 16 | }); 17 | } 18 | }); 19 | 20 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Autodesk 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 | 17 | import Molecule2d from './components/molecule_2d.jsx'; 18 | 19 | export default Molecule2d; 20 | -------------------------------------------------------------------------------- /example/js/plugins.js: -------------------------------------------------------------------------------- 1 | // Avoid `console` errors in browsers that lack a console. 2 | (function() { 3 | var method; 4 | var noop = function () {}; 5 | var methods = [ 6 | 'assert', 'clear', 'count', 'debug', 'dir', 'dirxml', 'error', 7 | 'exception', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log', 8 | 'markTimeline', 'profile', 'profileEnd', 'table', 'time', 'timeEnd', 9 | 'timeline', 'timelineEnd', 'timeStamp', 'trace', 'warn' 10 | ]; 11 | var length = methods.length; 12 | var console = (window.console = window.console || {}); 13 | 14 | while (length--) { 15 | method = methods[length]; 16 | 17 | // Only stub undefined methods. 18 | if (!console[method]) { 19 | console[method] = noop; 20 | } 21 | } 22 | }()); 23 | 24 | // Place any jQuery/helper plugins in here. 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6.4.0 4 | script: 5 | - npm test 6 | - npm run setup-selenium 7 | - npm run example & 8 | - npm run e2e 9 | deploy: 10 | provider: npm 11 | email: justinjmccandless@gmail.com 12 | api_key: 13 | secure: LCfY4dl0AI8fwb77PvjnL7LKzKGIkVFG2BbMxEipHHqH4X91xLSEvAYfafQsyYBspVc/erkupveOUC77aQe6FxdzSOiNlUD1x9hVFS0eQStZJpmAPj7YctSXWx5956ZW74aHSgVLdIjC68cl6FCAiymxKC5tRmPbNcUMOqLxMK1Tj+dGO83VIYz3nQxz9FuLMR6nCPTBUVu/+re9xlqmMqRj/b7S2GsuDxq00ReLl/5SHf4/o/ftmiOxHI8mFuayuJ4BnI/9JEytTRiUA40IkB8LRz8s/OnlVIcwSu7iGPKoeNiS+Mc+XWyIoKIGYCBRCnarZPelPlZk5CpUoH/S9G7H3hHeTiNLaVJpCrNKZ+hcpNqEifPOhegHAn1AqLVPqphfYG6Dt9uXAvPABDVrKqaVFAbQDZdfobIayfpxAm3pLl2ZEuKTSZfzzzY+yg75IAGE912d5dgGrkbqvZMVCVrhNbEBpKrHJrvb/MpsqvfhMwxgPPJu/xbWnTUhd7h8yyMXklNLoeDEHy4dcObQauxZmxEiqDaao+0AqlVQF6gSVtyfJxTAnBvgYJLQcACb3EBTQkjaPYuSDHt9AE6HsTRTzQLRbbTCzDW2F5i/EGdRD6uLHZUQDDnAMYXTrRYufIgfUdR3PkPMQFcX6kIGOFHuosxBGVFHSJKGkOtrJ8M= 14 | on: 15 | tags: true 16 | repo: Autodesk/molecule-2d-for-react 17 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const InlineEnviromentVariablesPlugin = require('inline-environment-variables-webpack-plugin'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | entry: { 6 | app: ['./src/main.js'], 7 | }, 8 | output: { 9 | path: path.resolve(__dirname, 'dist'), 10 | publicPath: '/js/', 11 | filename: 'bundle.js', 12 | library: true, 13 | libraryTarget: 'commonjs2', 14 | }, 15 | externals: [ 16 | /^[a-z\-0-9]+$/, 17 | ], 18 | module: { 19 | loaders: [ 20 | { 21 | test: /\.jsx?$/, 22 | exclude: /(node_modules|bower_components)/, 23 | loader: 'babel', 24 | }, { 25 | test: /\.jsx?$/, 26 | exclude: /(node_modules|bower_components)/, 27 | loader: 'eslint-loader', 28 | }, { 29 | test: /\.scss$/, 30 | loaders: ['style', 'css', 'sass'], 31 | }, 32 | ], 33 | }, 34 | devtool: 'source-map', 35 | plugins: [ 36 | new InlineEnviromentVariablesPlugin(), 37 | ], 38 | }; 39 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const InlineEnviromentVariablesPlugin = require('inline-environment-variables-webpack-plugin'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | entry: { 6 | example: ['babel-polyfill', './example/js/main.jsx'], 7 | }, 8 | output: { 9 | path: path.resolve(__dirname, 'dist'), 10 | publicPath: '/js/', 11 | filename: 'bundle.[name].js', 12 | }, 13 | devServer: { 14 | port: '4000', 15 | hot: true, 16 | }, 17 | module: { 18 | loaders: [ 19 | { 20 | test: /\.jsx?$/, 21 | exclude: /(node_modules|bower_components)/, 22 | loader: 'babel', 23 | }, { 24 | test: /\.jsx?$/, 25 | exclude: /(node_modules|bower_components)/, 26 | loader: 'eslint-loader', 27 | }, { 28 | test: /\.scss$/, 29 | loaders: ['style', 'css', 'sass'], 30 | }, 31 | ], 32 | }, 33 | devtool: 'source-map', 34 | plugins: [ 35 | new InlineEnviromentVariablesPlugin(), 36 | ], 37 | }; 38 | -------------------------------------------------------------------------------- /test/e2e/specs/selection_spec.js: -------------------------------------------------------------------------------- 1 | const setup = require('../fixtures/setup'); 2 | 3 | module.exports = { 4 | 'Selection Spec': (browser) => { 5 | setup(browser) 6 | .url(browser.launchUrl) 7 | .waitForElementVisible('.molecule-2d svg', 1000, 'molecule-2d SVG element appears') 8 | .waitForElementVisible('g.node', 1000, 'Nodes are rendered in the SVG.') 9 | .click('g.node') 10 | .assert.elementPresent('g.node.selected', 'Clicking a node adds a selected class to it') 11 | .click('g.node:last-child') 12 | // Clicking a different node selects both of them 13 | .elements('css selector', 'g.node.selected', (result) => { 14 | browser.expect(result.value.length).to.equal(2); 15 | }) 16 | .click('g.node') 17 | // Clicking a selected node unselects it 18 | .elements('css selector', 'g.node.selected', (result) => { 19 | browser.expect(result.value.length).to.equal(1); 20 | }) 21 | .click('g.node:last-child') 22 | .assert.elementNotPresent('g.node.selected', 'Clicking all selected nodes unselects them all') 23 | .end(); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /nightwatch.conf.js: -------------------------------------------------------------------------------- 1 | const BINPATH = './node_modules/nightwatch/bin/'; 2 | 3 | module.exports = { 4 | src_folders: [ 5 | 'test/e2e', // Where you are storing your Nightwatch e2e/UAT tests 6 | ], 7 | output_folder: './reports', // reports (test outcome) output by nightwatch 8 | selenium: { // downloaded by selenium-download module (see readme) 9 | start_process: true, // tells nightwatch to start/stop the selenium process 10 | server_path: `${BINPATH}selenium.jar`, 11 | log_path: '', 12 | host: '127.0.0.1', 13 | port: 4444, // standard selenium port 14 | }, 15 | test_settings: { 16 | default: { 17 | screenshots: { 18 | enabled: false, 19 | path: './screenshots', 20 | }, 21 | globals: { 22 | waitForConditionTimeout: 5000, // sometimes internet is slow so wait. 23 | }, 24 | launch_url: 'http://localhost:4000', 25 | desiredCapabilities: { 26 | browserName: 'phantomjs', 27 | javascriptEnabled: true, 28 | acceptSslCerts: true, 29 | 'phantomjs.binary.path': './node_modules/.bin/phantomjs', 30 | 'phantomjs.cli.args': [], 31 | }, 32 | }, 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 |
24 |

molecule-2d

25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /example/js/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import ExampleSettings from './example_settings.jsx'; 4 | import Molecule2d from '../../src/main.js'; 5 | import bipyridine from './bipyridine'; 6 | import residue from './residue'; 7 | 8 | class Example extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | selectedAtomIds: [], 14 | modelData: residue, 15 | }; 16 | } 17 | 18 | onChangeSelection = (selectedAtomIds) => { 19 | this.setState({ 20 | selectedAtomIds, 21 | }); 22 | } 23 | 24 | onToggleMolecule = () => { 25 | this.setState({ 26 | modelData: this.state.modelData === bipyridine ? residue : bipyridine, 27 | }); 28 | } 29 | 30 | render() { 31 | return ( 32 |
33 | 38 | 43 |
44 | ); 45 | } 46 | } 47 | 48 | render( 49 | , 50 | document.querySelector('.molecule-2d') 51 | ); 52 | -------------------------------------------------------------------------------- /example/js/residue.js: -------------------------------------------------------------------------------- 1 | const residue = { 2 | nodes: [ 3 | {"id":1000,"atom":"HE22"}, 4 | {"id":991,"atom":"C"}, 5 | {"id":997,"atom":"NE2"}, 6 | {"id":990,"atom":"CA"}, 7 | {"id":995,"atom":"CD"}, 8 | {"id":996,"atom":"OE1"}, 9 | {"id":993,"atom":"CB"}, 10 | {"id":992,"atom":"O"}, 11 | {"id":994,"atom":"CG"}, 12 | {"id":999,"atom":"HE21"}, 13 | {"id":989,"atom":"N"}, 14 | {"id":998,"atom":"H"} 15 | ], 16 | links: [ 17 | { id: 90, "source":989,"target":990,"bond":1, strength: 1, distance: 30.0 }, 18 | { id: 91, "source":989,"target":998,"bond":1, strength: 1, distance: 30.0 }, 19 | { id: 92, "source":990,"target":991,"bond":1, strength: 1, distance: 30.0 }, 20 | { id: 93, "source":990,"target":993,"bond":1, strength: 1, distance: 30.0 }, 21 | { id: 94, "source":991,"target":992,"bond":2, strength: 1, distance: 30.0 }, 22 | { id: 95, "source":993,"target":994,"bond":1, strength: 1, distance: 30.0 }, 23 | { id: 96, "source":994,"target":995,"bond":1, strength: 1, distance: 30.0 }, 24 | { id: 97, "source":995,"target":997,"bond":1, strength: 1, distance: 30.0 }, 25 | { id: 98, "source":995,"target":996,"bond":2, strength: 1, distance: 30.0 }, 26 | { id: 99, "source":997,"target":1000,"bond":1, strength: 1, distance: 30.0 }, 27 | { id: 100, "source":997,"target":999,"bond":1, strength: 1, distance: 30.0 } 28 | ], 29 | }; 30 | 31 | export default residue; 32 | -------------------------------------------------------------------------------- /example/js/example_settings.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class ExampleSettings extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | 7 | this.state = { 8 | selectedAtomIds: '', 9 | }; 10 | } 11 | 12 | componentWillReceiveProps(nextProps) { 13 | this.setState({ 14 | selectedAtomIds: JSON.stringify(nextProps.selectedAtomIds), 15 | }); 16 | } 17 | 18 | onChangeSelection = (event) => { 19 | this.setState({ 20 | selectedAtomIds: event.target.value, 21 | }); 22 | } 23 | 24 | onBlurSelection = (event) => { 25 | let selectedAtomIds; 26 | 27 | try { 28 | selectedAtomIds = JSON.parse(event.target.value); 29 | } catch (err) { 30 | throw err; 31 | } 32 | 33 | this.props.onChangeSelection(selectedAtomIds); 34 | } 35 | 36 | render() { 37 | return ( 38 |
39 | 42 |

selectedAtomIds

43 | 48 |
49 | ); 50 | } 51 | } 52 | 53 | ExampleSettings.propTypes = { 54 | selectedAtomIds: React.PropTypes.arrayOf(React.PropTypes.number), 55 | onChangeSelection: React.PropTypes.func, 56 | onToggleMolecule: React.PropTypes.func, 57 | }; 58 | 59 | export default ExampleSettings; 60 | -------------------------------------------------------------------------------- /src/utils/mol_view_utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Autodesk 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 | 17 | const molViewUtils = { 18 | getBondWidth(d) { 19 | if (!d || isNaN(parseInt(d.bond, 10))) { 20 | throw new Error('Invalid input'); 21 | } 22 | if (d.bond < 0) { 23 | throw new Error('d.bond must be at least 0'); 24 | } 25 | 26 | return `${(d.bond * 4) - 2}px`; 27 | }, 28 | 29 | chooseColor(d, defaultValue) { 30 | const color = d.category || d.color; 31 | if (color) { 32 | return color; 33 | } 34 | return defaultValue; 35 | }, 36 | 37 | withDefault(test, defaultValue) { 38 | if (typeof test === 'undefined') { 39 | return defaultValue; 40 | } 41 | 42 | return test; 43 | }, 44 | 45 | /* 46 | bondClickCallback(mywidget) { // not hooked up yet 47 | mywidget.model.set('clicked_bond_indices', 48 | [this.attributes.sourceIndex.value*1, 49 | this.attributes.targetIndex.value*1]); 50 | mywidget.model.save(); 51 | }, 52 | */ 53 | }; 54 | 55 | export default molViewUtils; 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "molecule-2d-for-react", 3 | "version": "0.2.3", 4 | "description": "2D molecule visualization using React and D3", 5 | "main": "dist/bundle.js", 6 | "keywords": [ 7 | "molecule", 8 | "chemical", 9 | "react", 10 | "visualization", 11 | "d3", 12 | "2d" 13 | ], 14 | "dependencies": { 15 | "d3": "^4.1.1", 16 | "react": "^15.3.2", 17 | "react-dom": "^15.3.2" 18 | }, 19 | "devDependencies": { 20 | "babel-core": "^6.7.7", 21 | "babel-eslint": "^7.0.0", 22 | "babel-loader": "^6.2.4", 23 | "babel-polyfill": "^6.16.0", 24 | "babel-preset-es2015": "^6.16.0", 25 | "babel-preset-es2016": "^6.16.0", 26 | "babel-preset-es2017": "^6.16.0", 27 | "babel-preset-react": "^6.11.1", 28 | "babel-preset-stage-2": "^6.17.0", 29 | "chai": "^3.5.0", 30 | "css-loader": "^0.25.0", 31 | "eslint": "^3.5.0", 32 | "eslint-config-airbnb": "^11.1.0", 33 | "eslint-loader": "^1.3.0", 34 | "eslint-plugin-import": "^1.15.0", 35 | "eslint-plugin-jsx-a11y": "^2.2.2", 36 | "eslint-plugin-react": "^6.4.1", 37 | "inline-environment-variables-webpack-plugin": "^1.1.0", 38 | "karma": "^0.13.22", 39 | "karma-mocha": "^0.2.2", 40 | "karma-phantomjs-launcher": "^1.0.0", 41 | "karma-webpack": "^1.7.0", 42 | "mocha": "^2.4.5", 43 | "nightwatch": "^0.9.8", 44 | "node-sass": "^3.9.3", 45 | "phantomjs-prebuilt": "^2.1.7", 46 | "sass-loader": "^4.0.2", 47 | "selenium-download": "^2.0.6", 48 | "sinon": "^2.0.0-pre.2", 49 | "style-loader": "^0.13.1", 50 | "webpack": "^1.13.2", 51 | "webpack-dev-server": "^1.14.1" 52 | }, 53 | "scripts": { 54 | "test": "karma start --single-run", 55 | "tdd": "karma start", 56 | "setup-selenium": "node scripts/download_selenium.js", 57 | "e2e": "./node_modules/nightwatch/bin/nightwatch", 58 | "build": "webpack -p --config webpack.config.js", 59 | "watch": "webpack --config webpack.config.js --watch", 60 | "example": "NODE_ENV=DEVELOPMENT webpack-dev-server --content-base example/ --config example/webpack.config.js --progress --colors", 61 | "prepublish": "npm run build" 62 | }, 63 | "author": "Autodesk Bio/Nano", 64 | "license": "Apache-2.0", 65 | "repository": { 66 | "type": "git", 67 | "url": "https://github.com/Autodesk/molecule-2d-for-react.git" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Sat Apr 30 2016 13:33:59 GMT-0700 (PDT) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['mocha'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | 'test/unit/**/*_spec.js' 19 | ], 20 | 21 | 22 | // list of files to exclude 23 | exclude: [ 24 | ], 25 | 26 | 27 | // preprocess matching files before serving them to the browser 28 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 29 | preprocessors: { 30 | 'test/unit/*_spec.js': ['webpack'], 31 | 'test/unit/**/*_spec.js': ['webpack'] 32 | }, 33 | 34 | 35 | webpack: { 36 | module: { 37 | loaders: [{ 38 | test: /\.(js|jsx)$/, exclude: /(bower_components|node_modules)/, 39 | loader: 'babel-loader' 40 | }, { 41 | test: /\.scss$/, 42 | include: /example\/css/, 43 | loaders: ['style', 'css', 'sass'], 44 | }] 45 | } 46 | }, 47 | 48 | 49 | // test results reporter to use 50 | // possible values: 'dots', 'progress' 51 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 52 | reporters: ['progress'], 53 | 54 | 55 | // web server port 56 | port: 9876, 57 | 58 | 59 | // enable / disable colors in the output (reporters and logs) 60 | colors: true, 61 | 62 | 63 | // level of logging 64 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 65 | logLevel: config.LOG_INFO, 66 | 67 | 68 | // enable / disable watching file and executing tests whenever any file changes 69 | autoWatch: true, 70 | 71 | 72 | // start these browsers 73 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 74 | browsers: ['PhantomJS'], 75 | 76 | 77 | // Continuous Integration mode 78 | // if true, Karma captures browsers, runs the tests and exits 79 | singleRun: false, 80 | 81 | // Concurrency level 82 | // how many browser should be started simultaneous 83 | concurrency: Infinity 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /test/unit/utils/mol_view_utils_spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, before, after, beforeEach, afterEach */ 2 | 3 | import { assert, expect } from 'chai'; 4 | import sinon from 'sinon'; 5 | import d3 from 'd3'; 6 | import molViewUtils from '../../../src/utils/mol_view_utils'; 7 | 8 | describe('molViewUtils', () => { 9 | describe('getBondWidth', () => { 10 | describe('when given invalid input', () => { 11 | it('throws an error', () => { 12 | assert.throws(molViewUtils.getBondWidth.bind(null, 'words')); 13 | assert.throws(molViewUtils.getBondWidth.bind(null, {})); 14 | assert.throws(molViewUtils.getBondWidth.bind(null, { bond: -1 })); 15 | }); 16 | }); 17 | 18 | describe('when given an object with a bond number', () => { 19 | it('returns a pixel string', () => { 20 | assert.equal(molViewUtils.getBondWidth({ bond: 4 }), '14px'); 21 | }); 22 | }); 23 | }); 24 | 25 | describe('chooseColor', () => { 26 | const color = '#abcdef'; 27 | let d = {}; 28 | 29 | beforeEach(() => { 30 | d = {}; 31 | }); 32 | 33 | describe('when d has a category', () => { 34 | beforeEach(() => { 35 | d.category = color; 36 | }); 37 | 38 | it('chooses the corresponding color from d3\'s color palette', () => { 39 | expect(molViewUtils.chooseColor(d)).to.equal(color); 40 | }); 41 | }); 42 | 43 | describe('when d has a color and not a category', () => { 44 | beforeEach(() => { 45 | d.color = color; 46 | }); 47 | 48 | it('returns d.color', () => { 49 | expect(molViewUtils.chooseColor(d)).to.equal(color); 50 | }); 51 | }); 52 | 53 | describe('when d has no color and no category', () => { 54 | it('returns defaultValue', () => { 55 | expect(molViewUtils.chooseColor(d, color)).to.equal(color); 56 | }); 57 | }); 58 | }); 59 | 60 | describe('withDefault', () => { 61 | const defaultValue = 'imadefaultValue'; 62 | let test; 63 | 64 | describe('when given an undefined test value', () => { 65 | before(() => { 66 | test = undefined; 67 | }); 68 | 69 | it('returns defaultValue', () => { 70 | assert.equal(molViewUtils.withDefault(undefined, defaultValue), defaultValue); 71 | }); 72 | }); 73 | 74 | describe('when give a test value that\'s not undefined', () => { 75 | before(() => { 76 | test = 'imatestval'; 77 | }); 78 | 79 | it('returns test', () => { 80 | assert.equal(molViewUtils.withDefault(test, defaultValue), test); 81 | }); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /src/components/nodes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | drag, 4 | event as d3Event, 5 | scaleSqrt, 6 | select, 7 | selectAll, 8 | } from 'd3'; 9 | import molViewUtils from '../utils/mol_view_utils'; 10 | 11 | require('../styles/nodes.scss'); 12 | 13 | const SELECTED_COLOR = '#39f8ff'; 14 | 15 | class Nodes extends React.Component { 16 | static onDragged(d) { 17 | d.fx = d3Event.x; 18 | d.fy = d3Event.y; 19 | } 20 | 21 | componentDidMount() { 22 | this.renderD3(); 23 | } 24 | 25 | componentDidUpdate() { 26 | this.renderD3(); 27 | } 28 | 29 | renderD3() { 30 | if (!this.nodesContainer) { 31 | return; 32 | } 33 | 34 | const container = select(this.nodesContainer); 35 | 36 | const nodes = container.selectAll('.node') 37 | .data(this.props.nodes, d => d.id); 38 | 39 | const newNodesG = nodes.enter().append('g'); 40 | 41 | newNodesG 42 | .attr('class', 'node') 43 | .on('click', this.props.onClickNode) 44 | .attr('index', d => d.id) 45 | .call(drag() 46 | .on('start', this.props.onDragStartedNode) 47 | .on('drag', Nodes.onDragged) 48 | .on('end', this.props.onDragEndedNode) 49 | ); 50 | 51 | container.selectAll('.node') 52 | .classed('selected', d => 53 | (this.props.selectedAtomIds.indexOf(d.id) !== -1 ? SELECTED_COLOR : '') 54 | ); 55 | 56 | const radius = scaleSqrt().range([0, 6]); 57 | 58 | // circle for each atom (background color white by default) 59 | newNodesG.append('circle') 60 | .attr('class', 'atom-circle') 61 | .attr('r', d => radius(molViewUtils.withDefault(d.size, 1.5))) 62 | .style('fill', d => molViewUtils.chooseColor(d, 'white')); 63 | selectAll('.atom-circle') 64 | .style('stroke', d => 65 | (this.props.selectedAtomIds.indexOf(d.id) !== -1 ? SELECTED_COLOR : '') 66 | ); 67 | 68 | // atom labels 69 | newNodesG.append('text') 70 | .attr('class', 'atom-label') 71 | .attr('dy', '.35em') 72 | .attr('text-anchor', 'middle') 73 | .text(d => d.atom); 74 | container.selectAll('.atom-label') 75 | .attr('fill', (d) => { 76 | const color = this.props.selectedAtomIds.indexOf(d.id) !== -1 ? 77 | SELECTED_COLOR : ''; 78 | return molViewUtils.withDefault(d.textcolor, color); 79 | }); 80 | 81 | nodes.exit() 82 | .remove(); 83 | } 84 | 85 | render() { 86 | return ( 87 | { this.nodesContainer = c; }} /> 88 | ); 89 | } 90 | } 91 | 92 | Nodes.propTypes = { 93 | selectedAtomIds: React.PropTypes.arrayOf(React.PropTypes.number), 94 | nodes: React.PropTypes.arrayOf(React.PropTypes.object), 95 | onClickNode: React.PropTypes.func, 96 | onDragStartedNode: React.PropTypes.func, 97 | onDragEndedNode: React.PropTypes.func, 98 | }; 99 | 100 | export default Nodes; 101 | -------------------------------------------------------------------------------- /src/components/links.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | select, 4 | } from 'd3'; 5 | import molViewUtils from '../utils/mol_view_utils'; 6 | 7 | require('../styles/links.scss'); 8 | 9 | class Links extends React.Component { 10 | componentDidMount() { 11 | this.renderD3(); 12 | } 13 | 14 | componentDidUpdate() { 15 | this.renderD3(); 16 | } 17 | 18 | renderD3() { 19 | if (!this.linksContainer) { 20 | return; 21 | } 22 | 23 | const container = select(this.linksContainer); 24 | 25 | const links = container 26 | .selectAll('.link') 27 | .data(this.props.links, d => d.id); 28 | 29 | const newLinksG = links.enter() 30 | .append('g') 31 | .attr('class', 'link'); 32 | 33 | // all edges (includes both bonds and distance constraints) 34 | newLinksG 35 | .append('line') 36 | .attr('class', 'link-line'); 37 | container.selectAll('.link-line') 38 | .attr('source', d => 39 | (typeof d.source.id !== 'undefined' ? d.source.id : d.source) 40 | ) 41 | .attr('target', d => (typeof d.target.id !== 'undefined' ? d.target.id : d.target)) 42 | .style('stroke-width', molViewUtils.getBondWidth) 43 | .style('stroke-dasharray', (d) => { 44 | if (d.style === 'dashed') { 45 | return 5; 46 | } 47 | 48 | return 0; 49 | }) 50 | .style('stroke', d => molViewUtils.chooseColor(d, 'black')) 51 | .style('opacity', (d) => { 52 | if (d.bond !== 0) { 53 | return undefined; 54 | } 55 | return 0.0; 56 | }); 57 | 58 | // text placeholders for all edges 59 | newLinksG 60 | .append('text'); 61 | container.selectAll('.link').selectAll('text') 62 | .attr('x', d => d.source.x || 0) 63 | .attr('y', d => d.source.y || 0) 64 | .attr('text-anchor', 'middle') 65 | .text(() => ' '); 66 | 67 | // double and triple bonds 68 | newLinksG 69 | .filter(d => d.bond > 1) 70 | .append('line') 71 | .attr('class', 'separator separator-double'); 72 | container.selectAll('.separator-double') 73 | .style('stroke', '#FFF') 74 | .style('stroke-width', d => `${(d.bond * 4) - 5}px`); 75 | 76 | // triple bonds 77 | newLinksG 78 | .filter(d => d.bond === 3) 79 | .append('line') 80 | .attr('class', 'separator separator-triple'); 81 | container.selectAll('.separator-triple') 82 | .style('stroke', d => molViewUtils.chooseColor(d, 'black')) 83 | .style('stroke-width', () => molViewUtils.getBondWidth(1)); 84 | 85 | links.exit() 86 | .remove(); 87 | } 88 | 89 | render() { 90 | return ( 91 | { this.linksContainer = c; }} /> 92 | ); 93 | } 94 | } 95 | 96 | Links.propTypes = { 97 | links: React.PropTypes.arrayOf(React.PropTypes.object), 98 | }; 99 | 100 | export default Links; 101 | -------------------------------------------------------------------------------- /src/utils/molecule_utils.js: -------------------------------------------------------------------------------- 1 | const moleculeUtils = { 2 | /** 3 | * Given two arrays of ids, return true if they contain the same values in any order, 4 | * ignoring duplicates 5 | * @param idsA {Array} 6 | * @param idsB {Array} 7 | * @returns {Boolean} 8 | */ 9 | compareIds(idsA, idsB) { 10 | // If one array is empty and the other isn't, they can't contain the same values 11 | if ((!idsA.length && idsB.length) || (idsA.length && !idsB.length)) { 12 | return false; 13 | } 14 | 15 | const mapA = new Map(); 16 | idsA.forEach((id) => { 17 | mapA.set(id, false); 18 | }); 19 | 20 | for (const id of idsB) { 21 | // If an id exists in B but not in A, not equivalent 22 | if (!mapA.has(id)) { 23 | return false; 24 | } 25 | mapA.set(id, true); 26 | } 27 | 28 | // If an id exists in A but not B, not equivalent 29 | for (const tuple of mapA) { 30 | const value = tuple[1]; 31 | if (!value) { 32 | return false; 33 | } 34 | } 35 | 36 | return true; 37 | }, 38 | 39 | /** 40 | * Due to craziness of D3, we need to keep our main modelData state as the same object and mutate 41 | * it in place. The same goes for all sub-objects within modelData. 42 | * @param oldModelData {Object} 43 | * @param newModelData {Object} 44 | * @returns {Object} 45 | */ 46 | updateObjectInPlace(oldObject, newObject) { 47 | Object.keys(newObject).forEach((key) => { 48 | if (oldObject[key] instanceof Object && newObject[key] instanceof Object) { 49 | oldObject[key] = moleculeUtils.updateObjectInPlace(oldObject[key], newObject[key]); 50 | } else { 51 | oldObject[key] = newObject[key]; 52 | } 53 | }); 54 | 55 | return oldObject; 56 | }, 57 | 58 | /** 59 | * Given old and new arrays of models, update the old array's models in place based on id 60 | * If model ids don't perfectly match, just return newArray 61 | * O(n^2) :( 62 | * @param oldArray {Array} 63 | * @param newArray {Array} 64 | * @returns {Array} 65 | */ 66 | updateModels(oldArray, newArray) { 67 | const sameIds = moleculeUtils.compareIds( 68 | oldArray.map(model => model.id), newArray.map(model => model.id) 69 | ); 70 | if (!sameIds) { 71 | return newArray; 72 | } 73 | 74 | // Add or update everything in newArray to oldArray 75 | newArray.forEach((newModel) => { 76 | let found = false; 77 | for (let i = 0; i < oldArray.length; i += 1) { 78 | if (oldArray[i].id === newModel.id) { 79 | oldArray[i] = moleculeUtils.updateObjectInPlace(oldArray[i], newModel); 80 | found = true; 81 | break; 82 | } 83 | } 84 | 85 | if (!found) { 86 | oldArray.push(newModel); 87 | } 88 | }); 89 | 90 | // Remove els in oldArray that don't exist in newArray 91 | for (let i = 0; i < oldArray.length; i += 1) { 92 | const oldModel = oldArray[i]; 93 | const newModel = newArray.find(newModelI => newModelI.id === oldModel.id); 94 | 95 | if (!newModel) { 96 | oldArray.splice(i, 1); 97 | i -= 1; 98 | } 99 | } 100 | 101 | return oldArray; 102 | }, 103 | }; 104 | 105 | export default moleculeUtils; 106 | -------------------------------------------------------------------------------- /example/js/bipyridine.js: -------------------------------------------------------------------------------- 1 | const bipyridine = { 2 | nodes: [ 3 | { id: 0, atom: 'N' }, 4 | { id: 1, atom: 'C' }, 5 | { id: 2, atom: 'C3' }, 6 | { id: 3, atom: 'C4' }, 7 | { id: 4, atom: 'C5' }, 8 | { id: 5, atom: 'C6' }, 9 | { id: 6, atom: 'C7' }, 10 | { id: 7, atom: 'N8' }, 11 | { id: 8, atom: 'C9' }, 12 | { id: 9, atom: 'C10' }, 13 | { id: 10, atom: 'C11' }, 14 | { id: 11, atom: 'C12' }, 15 | ], 16 | links: [ 17 | { id: 0, source: 1, strength: 1, distance: 29.91570726558207, target: 0, bond: 2 }, 18 | { id: 1, source: 2, strength: 1, distance: 30.870992090958136, target: 1, bond: 1 }, 19 | { id: 2, source: 3, strength: 1, distance: 30.669623995738846, target: 2, bond: 2 }, 20 | { id: 3, source: 4, strength: 1, distance: 30.320011541554535, target: 3, bond: 1 }, 21 | { id: 4, source: 5, strength: 1, distance: 30.462333981492616, target: 4, bond: 2 }, 22 | { id: 5, source: 5, strength: 1, distance: 29.942081226928764, target: 0, bond: 1 }, 23 | { id: 6, source: 6, strength: 1, distance: 33.10311986323948, target: 1, bond: 1 }, 24 | { id: 7, source: 7, strength: 1, distance: 29.914643005725473, target: 6, bond: 2 }, 25 | { id: 8, source: 8, strength: 1, distance: 29.94415643961273, target: 7, bond: 1 }, 26 | { id: 9, source: 9, strength: 1, distance: 30.46307691747502, target: 8, bond: 2 }, 27 | { id: 10, source: 10, strength: 1, distance: 30.317225221975708, target: 9, bond: 1 }, 28 | { id: 11, source: 11, strength: 1, distance: 30.670350548371626, target: 10, bond: 2 }, 29 | { id: 12, source: 11, strength: 1, distance: 30.87016403843685, target: 6, bond: 1 }, 30 | { id: 13, bond: 0, source: 0, strength: 0.66, target: 2, distance: 52.55554066661288 }, 31 | { id: 14, bond: 0, source: 0, strength: 0.66, target: 3, distance: 61.44022025806874 }, 32 | { id: 15, bond: 0, source: 0, strength: 0.66, target: 4, distance: 53.38188235384736 }, 33 | { id: 16, bond: 0, source: 0, strength: 0.66, target: 6, distance: 54.29639315497853 }, 34 | { id: 17, bond: 0, source: 0, strength: 0.66, target: 7, distance: 62.034165551895676 }, 35 | { id: 18, bond: 0, source: 0, strength: 0.32917806598897, target: 8, distance: 91.85214425042018 }, 36 | { id: 19, bond: 0, source: 0, strength: 0.4360951942958962, target: 11, distance: 82.46783744539444 }, 37 | { id: 20, bond: 0, source: 1, strength: 0.66, target: 3, distance: 53.510594708711665 }, 38 | { id: 21, bond: 0, source: 1, strength: 0.66, target: 4, distance: 61.24937170518568 }, 39 | { id: 22, bond: 0, source: 1, strength: 0.66, target: 5, distance: 51.3799317808033 }, 40 | { id: 23, bond: 0, source: 1, strength: 0.66, target: 7, distance: 54.30254153057663 }, 41 | { id: 24, bond: 0, source: 1, strength: 0.4462654710317949, target: 8, distance: 81.63853562307449 }, 42 | { id: 25, bond: 0, source: 1, strength: 0.30494498544674653, target: 9, distance: 94.18343738917156 }, 43 | { id: 26, bond: 0, source: 1, strength: 0.41980042870488044, target: 10, distance: 83.81696527314742 }, 44 | { id: 27, bond: 0, source: 1, strength: 0.66, target: 11, distance: 55.75563128115401 }, 45 | { id: 28, bond: 0, source: 2, strength: 0.66, target: 4, distance: 52.576423819807296 }, 46 | { id: 29, bond: 0, source: 2, strength: 0.66, target: 5, distance: 59.387845223075736 }, 47 | { id: 30, bond: 0, source: 2, strength: 0.66, target: 6, distance: 55.76335213238171 }, 48 | { id: 31, bond: 0, source: 2, strength: 0.4358650877208918, target: 7, distance: 82.48671198587078 }, 49 | { id: 32, bond: 0, source: 2, strength: 0.29105396519486687, target: 10, distance: 95.56171467590984 }, 50 | { id: 33, bond: 0, source: 2, strength: 0.66, target: 11, distance: 65.13393127333862 }, 51 | { id: 34, bond: 0, source: 3, strength: 0.66, target: 5, distance: 52.01413064966097 }, 52 | { id: 35, bond: 0, source: 3, strength: 0.41980270594235247, target: 6, distance: 83.81677491695801 }, 53 | { id: 36, bond: 0, source: 3, strength: 0.29131452162971666, target: 11, distance: 95.53556305648699 }, 54 | { id: 37, bond: 0, source: 4, strength: 0.305018202617337, target: 6, distance: 94.17625685362525 }, 55 | { id: 38, bond: 0, source: 5, strength: 0.4463947690054851, target: 6, distance: 81.62805361883866 }, 56 | { id: 39, bond: 0, source: 5, strength: 0.32911474065283325, target: 7, distance: 91.85812235703492 }, 57 | { id: 40, bond: 0, source: 6, strength: 0.66, target: 8, distance: 51.38270764449846 }, 58 | { id: 41, bond: 0, source: 6, strength: 0.66, target: 9, distance: 61.24948063583885 }, 59 | { id: 42, bond: 0, source: 6, strength: 0.66, target: 10, distance: 53.51103126085311 }, 60 | { id: 43, bond: 0, source: 7, strength: 0.66, target: 9, distance: 53.381539040383615 }, 61 | { id: 44, bond: 0, source: 7, strength: 0.66, target: 10, distance: 61.439202821325736 }, 62 | { id: 45, bond: 0, source: 7, strength: 0.66, target: 11, distance: 52.554700448960794 }, 63 | { id: 46, bond: 0, source: 8, strength: 0.66, target: 10, distance: 52.0157091536778 }, 64 | { id: 47, bond: 0, source: 8, strength: 0.66, target: 11, distance: 59.39079652404067 }, 65 | { id: 48, bond: 0, source: 9, strength: 0.66, target: 11, distance: 52.57455310699274 }, 66 | ], 67 | }; 68 | 69 | export default bipyridine; 70 | -------------------------------------------------------------------------------- /src/components/molecule_2d.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Autodesk 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 | import React from 'react'; 17 | import { 18 | event as d3Event, 19 | forceLink, 20 | forceManyBody, 21 | forceSimulation, 22 | forceCenter, 23 | select, 24 | } from 'd3'; 25 | import Nodes from '../components/nodes.jsx'; 26 | import Links from '../components/links.jsx'; 27 | import moleculeUtils from '../utils/molecule_utils'; 28 | import molViewUtils from '../utils/mol_view_utils'; 29 | 30 | class Molecule2d extends React.Component { 31 | constructor(props) { 32 | super(props); 33 | 34 | this.state = { 35 | selectedAtomIds: props.selectedAtomIds || [], 36 | }; 37 | } 38 | 39 | componentDidMount() { 40 | this.renderD3(); 41 | } 42 | 43 | componentWillReceiveProps(nextProps) { 44 | if (!moleculeUtils.compareIds(this.state.selectedAtomIds, nextProps.selectedAtomIds)) { 45 | this.setState({ 46 | selectedAtomIds: nextProps.selectedAtomIds, 47 | }); 48 | } 49 | } 50 | 51 | componentDidUpdate() { 52 | this.renderD3(); 53 | } 54 | 55 | onClickNode = (node) => { 56 | const selectedAtomIds = this.state.selectedAtomIds.slice(0); 57 | const index = selectedAtomIds.indexOf(node.id); 58 | 59 | if (index !== -1) { 60 | selectedAtomIds.splice(index, 1); 61 | } else { 62 | selectedAtomIds.push(node.id); 63 | } 64 | 65 | this.setState({ 66 | selectedAtomIds, 67 | }); 68 | 69 | this.props.onChangeSelection(selectedAtomIds); 70 | } 71 | 72 | onDragStartedNode = (d) => { 73 | if (!d3Event.active) { 74 | this.simulation.alphaTarget(0.3).restart(); 75 | } 76 | d.fx = d.x; 77 | d.fy = d.y; 78 | } 79 | 80 | onDragEndedNode = (d) => { 81 | if (!d3Event.active) { 82 | this.simulation.alphaTarget(0); 83 | } 84 | d.fx = null; 85 | d.fy = null; 86 | } 87 | 88 | renderTransform = () => { 89 | if (!this.svg) { 90 | return; 91 | } 92 | 93 | const container = select(this.svg); 94 | 95 | // Nodes 96 | container.selectAll('.node') 97 | .attr('transform', d => 98 | `translate(${d.x || 0},${d.y || 0})` 99 | ); 100 | 101 | // Links 102 | const links = container.selectAll('.link'); 103 | 104 | // keep edges pinned to their nodes 105 | links.selectAll('line') 106 | .attr('x1', d => d.source.x || 0) 107 | .attr('y1', d => d.source.y || 0) 108 | .attr('x2', d => d.target.x || 0) 109 | .attr('y2', d => d.target.y || 0); 110 | 111 | // keep edge labels pinned to the edges 112 | links.selectAll('text') 113 | .attr('x', d => 114 | ((d.source.x || 0) + (d.target.x || 0)) / 2.0 115 | ) 116 | .attr('y', d => 117 | ((d.source.y || 0) + (d.target.y || 0)) / 2.0 118 | ); 119 | } 120 | 121 | renderD3() { 122 | this.simulation = forceSimulation() 123 | .force('link', forceLink() 124 | .distance(d => molViewUtils.withDefault(d.distance, 20)) 125 | .strength(d => molViewUtils.withDefault(d.strength, 1.0)) 126 | ) 127 | .force('charge', forceManyBody()) 128 | .force('center', forceCenter(this.props.width / 2, this.props.height / 2)); 129 | 130 | this.simulation 131 | .nodes(this.nodes) 132 | .on('tick', () => this.renderTransform()); 133 | 134 | this.simulation.force('link') 135 | .id(d => d.id) 136 | .links(this.links); 137 | } 138 | 139 | render() { 140 | this.nodes = moleculeUtils.updateModels(this.nodes || [], this.props.modelData.nodes); 141 | this.links = moleculeUtils.updateModels(this.links || [], this.props.modelData.links); 142 | 143 | return ( 144 | { this.svg = c; }} 146 | width={this.props.width} 147 | height={this.props.height} 148 | > 149 | 152 | 159 | 160 | ); 161 | } 162 | } 163 | 164 | Molecule2d.defaultProps = { 165 | width: 500.0, 166 | height: 500.0, 167 | selectedAtomIds: [], 168 | onChangeSelection: () => {}, 169 | }; 170 | 171 | Molecule2d.propTypes = { 172 | width: React.PropTypes.number, 173 | height: React.PropTypes.number, 174 | modelData: React.PropTypes.shape({ 175 | nodes: React.PropTypes.array, 176 | links: React.PropTypes.array, 177 | }).isRequired, 178 | selectedAtomIds: React.PropTypes.arrayOf(React.PropTypes.number), 179 | onChangeSelection: React.PropTypes.func, 180 | }; 181 | 182 | export default Molecule2d; 183 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Molecule2d 2 | [![Build Status](https://travis-ci.org/Autodesk/molecule-2d-for-react.svg?branch=master)](https://travis-ci.org/Autodesk/molecule-2d-for-react) 3 | 4 | This project provides a React component that displays an interactive 2D representation of any molecule using D3. 5 | 6 | screen shot 7 | 8 | ## Installation 9 | 10 | npm install molecule-2d-for-react 11 | 12 | ## Usage 13 | 25 | 26 | See example/js/main.js for a working example. 27 | 28 | ## Props 29 | In order to set up your molecule visualization, just pass in the proper props data to the React component. Here are all of the parameters with explanations: 30 | 31 | ### modelData {Object} Required 32 | An object indicating the atoms an bonds to display. Of the form: 33 | 34 | { 35 | nodes: [ 36 | { id: 0, atom: 'H' }, 37 | ... 38 | ], 39 | links: [ 40 | { id: 0, source: 0, target: 1, strength: 1, distance: 30.0, bond: 1 }, 41 | ... 42 | ], 43 | } 44 | 45 | See example/js/bipyridine.js for an example of working modelData for a real molecule. 46 | 47 | ### selectedAtomIds {Array of Numbers} [[]] 48 | An array of atom ids to display as selected. This is deep copied into internal state and updated whenever the user clicks on an atom. See the `onChangeSelection` method below for how to listen to selection changes. 49 | 50 | ### width {Number} [500] 51 | The width of the SVG element. 52 | 53 | ### height {Number} [500] 54 | The height of the SVG element. 55 | 56 | ### onChangeSelection {Function} 57 | Called whenever selectedAtomIds is changed. Passed selectedAtomIds. 58 | 59 | ## Use in a Jupyter notebook 60 | It's also very easy to adapt this to work in a Jupyter Notebook as an [ipywidgets](https://github.com/ipython/ipywidgets) module, as it was made for the [Molecular Design Toolkit](https://github.com/Autodesk/molecular-design-toolkit) project. The [source code](https://github.com/Autodesk/notebook-molecular-visualization/blob/30e843393135d8b2d78ac055a6e366eb9c0ffde9/js/src/nbmolviz_2d_component.jsx) shows how this was done by wrapping this project in a Backbone view. 61 | 62 | ## What about 3d? 63 | Take a look at our sister project, [molecule-3d-for-react](https://github.com/Autodesk/molecule-3d-for-react), for a React component with a similar interface that renders a 3d visualization. 64 | 65 | ## Development 66 | A typical development flow might be to run the example while editing the code, where you'll want any changes to be immediately reflected in the example running in the browser. In that case you should run: 67 | 68 | npm run example 69 | 70 | ### Development within another project 71 | If you're using this in another project and want to make changes to this repository locally and see them reflected in your other project, first you'll need to do some setup. You can point your other project to use the local copy of Molecule2d like this: 72 | 73 | cd ~/path/to/molecule-2d-for-react 74 | npm link 75 | cd ~/path/to/other-project 76 | npm link molecule-2d-for-react 77 | 78 | See [this great blog post](http://justjs.com/posts/npm-link-developing-your-own-npm-modules-without-tears) for more info on `npm link`. 79 | 80 | Once you've linked your other project, you'll need to build Molecule2d (and likely your other project, too) every time you want your changes to reflect in your other project. You can do this manually with `npm run build`. If you want to rebuild Molecule2d automatically every time a change is made, run `npm run watch`. 81 | 82 | ### Running Tests 83 | Unit tests can be run with: 84 | 85 | npm test 86 | 87 | End-to-end tests can be run with: 88 | 89 | npm run e2e 90 | 91 | ### Releasing a new version 92 | Travis automatically publishes any new tagged commit to NPM. The best way to take advantage of this is to first create a new tagged commit using `npm version`: 93 | 94 | npm version patch -m "Upgrade to %s for reasons" 95 | 96 | Then push that commit to a new release branch, push the tag with `git push origin --tags` and open a pull request on Github. When you see that Travis has succeeded in deploying, merge it to master. 97 | 98 | ## License 99 | 100 | Copyright 2016 Autodesk Inc. 101 | 102 | 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 103 | 104 | http://www.apache.org/licenses/LICENSE-2.0 105 | 106 | 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. 107 | 108 | 109 | ## Contributing 110 | 111 | This project is developed and maintained by the [Molecular Design Toolkit](https://github.com/autodesk/molecular-design-toolkit) project. Please see that project's [CONTRIBUTING document](https://github.com/autodesk/molecular-design-toolkit/CONTRIBUTING.md) for details. 112 | -------------------------------------------------------------------------------- /test/unit/utils/molecule_utils_spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, before, after, beforeEach, afterEach */ 2 | 3 | import { assert, expect } from 'chai'; 4 | import 'babel-polyfill'; 5 | import moleculeUtils from '../../../src/utils/molecule_utils'; 6 | 7 | describe('moleculeUtils', () => { 8 | describe('compareIds', () => { 9 | let idsA; 10 | let idsB; 11 | 12 | describe('when given empty arrays', () => { 13 | beforeEach(() => { 14 | idsA = []; 15 | idsB = []; 16 | }); 17 | 18 | it('returns true', () => { 19 | expect(moleculeUtils.compareIds(idsA, idsB)).to.equal(true); 20 | }); 21 | }); 22 | 23 | describe('when one array is empty and the other isn\'t', () => { 24 | beforeEach(() => { 25 | idsA = [1]; 26 | idsB = []; 27 | }); 28 | 29 | it('returns false', () => { 30 | expect(moleculeUtils.compareIds(idsA, idsB)).to.equal(false); 31 | }); 32 | }); 33 | 34 | describe('when arrays have totally different values', () => { 35 | beforeEach(() => { 36 | idsA = [1, 2, 3]; 37 | idsB = [4, 5, 6]; 38 | }); 39 | 40 | it('returns false', () => { 41 | expect(moleculeUtils.compareIds(idsA, idsB)).to.equal(false); 42 | }); 43 | }); 44 | 45 | describe('when arrays have some (but not all) overlapping values', () => { 46 | beforeEach(() => { 47 | idsA = [1, 2, 3]; 48 | idsB = [2, 3, 4]; 49 | }); 50 | 51 | it('returns false', () => { 52 | expect(moleculeUtils.compareIds(idsA, idsB)).to.equal(false); 53 | }); 54 | }); 55 | 56 | describe('when arrays have same exact values in order', () => { 57 | beforeEach(() => { 58 | idsA = [1, 2, 3]; 59 | idsB = [1, 2, 3]; 60 | }); 61 | 62 | it('returns false', () => { 63 | expect(moleculeUtils.compareIds(idsA, idsB)).to.equal(true); 64 | }); 65 | }); 66 | 67 | describe('when arrays have same exact values in different order', () => { 68 | beforeEach(() => { 69 | idsA = [1, 2, 3]; 70 | idsB = [2, 3, 1]; 71 | }); 72 | 73 | it('returns false', () => { 74 | expect(moleculeUtils.compareIds(idsA, idsB)).to.equal(true); 75 | }); 76 | }); 77 | 78 | describe('when arrays have same values with duplicates', () => { 79 | beforeEach(() => { 80 | idsA = [1, 2, 3]; 81 | idsB = [2, 3, 1, 1, 1, 1, 2]; 82 | }); 83 | 84 | it('returns false', () => { 85 | expect(moleculeUtils.compareIds(idsA, idsB)).to.equal(true); 86 | }); 87 | }); 88 | 89 | describe('when arrays have some (not all) same values with duplicates', () => { 90 | beforeEach(() => { 91 | idsA = [1, 2, 3]; 92 | idsB = [2, 3, 1, 1, 1, 1, 2, 4]; 93 | }); 94 | 95 | it('returns false', () => { 96 | expect(moleculeUtils.compareIds(idsA, idsB)).to.equal(false); 97 | }); 98 | }); 99 | 100 | describe('when B has some (not all) same values with duplicates with A', () => { 101 | beforeEach(() => { 102 | idsA = [2, 3, 1, 1, 1, 1, 2, 4]; 103 | idsB = [1, 2, 3]; 104 | }); 105 | 106 | it('returns false', () => { 107 | expect(moleculeUtils.compareIds(idsA, idsB)).to.equal(false); 108 | }); 109 | }); 110 | }); 111 | 112 | describe('updateObjectInPlace', () => { 113 | let oldObject; 114 | let newObject; 115 | 116 | describe('when given a flat object', () => { 117 | beforeEach(() => { 118 | oldObject = { 119 | one: 1, 120 | two: 2, 121 | }; 122 | newObject = { 123 | one: 33, 124 | two: 'fdsa', 125 | four: 'what?', 126 | }; 127 | }); 128 | 129 | it('returns the same object but with newObject\'s data', () => { 130 | const result = moleculeUtils.updateObjectInPlace(oldObject, newObject); 131 | expect(result).to.equal(oldObject); 132 | expect(result.one).to.equal(newObject.one); 133 | expect(result.two).to.equal(newObject.two); 134 | expect(result.four).to.equal(newObject.four); 135 | }); 136 | }); 137 | 138 | describe('when given an object with a nested object', () => { 139 | beforeEach(() => { 140 | oldObject = { 141 | one: 1, 142 | obj: { 143 | two: 2, 144 | }, 145 | }; 146 | newObject = { 147 | one: 33, 148 | obj: { 149 | two: 22, 150 | }, 151 | }; 152 | }); 153 | 154 | it('returns the same object but with newObject\'s data at the first level', () => { 155 | const result = moleculeUtils.updateObjectInPlace(oldObject, newObject); 156 | expect(result).to.equal(oldObject); 157 | expect(result.one).to.equal(newObject.one); 158 | }); 159 | 160 | it('returns an object whose nested object is the same as oldObject\'s but with newObject\'s data', () => { 161 | const result = moleculeUtils.updateObjectInPlace(oldObject, newObject); 162 | expect(result.obj).to.equal(oldObject.obj); 163 | expect(result.obj.two).to.equal(newObject.obj.two); 164 | }); 165 | }); 166 | 167 | describe('when given a newObject that isn\'t an object', () => { 168 | beforeEach(() => { 169 | oldObject = { 170 | one: {}, 171 | }; 172 | newObject = { 173 | one: 'wait im not an object...', 174 | }; 175 | }); 176 | 177 | it('returns newObject', () => { 178 | const result = moleculeUtils.updateObjectInPlace(oldObject, newObject); 179 | expect(result).to.equal(oldObject); 180 | expect(result.one).to.equal(newObject.one); 181 | }); 182 | }); 183 | }); 184 | 185 | describe('updateModels', () => { 186 | let oldModels; 187 | let newModels; 188 | 189 | describe('when given mismatching arrays', () => { 190 | beforeEach(() => { 191 | oldModels = []; 192 | newModels = [ 193 | { id: 0 }, 194 | ]; 195 | }); 196 | 197 | it('returns newModels', () => { 198 | const result = moleculeUtils.updateModels(oldModels, newModels); 199 | expect(result).to.equal(newModels); 200 | expect(result[0]).to.equal(newModels[0]); 201 | }); 202 | }); 203 | 204 | describe('when given an existing element', () => { 205 | beforeEach(() => { 206 | oldModels = [ 207 | { id: 0 }, 208 | ]; 209 | newModels = [ 210 | { id: 0, something: true }, 211 | ]; 212 | }); 213 | 214 | it('returns oldModels with that element modified with new data', () => { 215 | const result = moleculeUtils.updateModels(oldModels, newModels); 216 | expect(result).to.equal(oldModels); 217 | expect(result[0]).to.equal(oldModels[0]); 218 | expect(result[0].something).to.equal(newModels[0].something); 219 | }); 220 | }); 221 | }); 222 | }); 223 | -------------------------------------------------------------------------------- /example/css/main.css: -------------------------------------------------------------------------------- 1 | /*! HTML5 Boilerplate v5.3.0 | MIT License | https://html5boilerplate.com/ */ 2 | 3 | /* 4 | * What follows is the result of much research on cross-browser styling. 5 | * Credit left inline and big thanks to Nicolas Gallagher, Jonathan Neal, 6 | * Kroc Camen, and the H5BP dev community and team. 7 | */ 8 | 9 | /* ========================================================================== 10 | Base styles: opinionated defaults 11 | ========================================================================== */ 12 | 13 | html { 14 | color: #222; 15 | font-size: 1em; 16 | line-height: 1.4; 17 | } 18 | 19 | /* 20 | * Remove text-shadow in selection highlight: 21 | * https://twitter.com/miketaylr/status/12228805301 22 | * 23 | * These selection rule sets have to be separate. 24 | * Customize the background color to match your design. 25 | */ 26 | 27 | ::-moz-selection { 28 | background: #b3d4fc; 29 | text-shadow: none; 30 | } 31 | 32 | ::selection { 33 | background: #b3d4fc; 34 | text-shadow: none; 35 | } 36 | 37 | /* 38 | * A better looking default horizontal rule 39 | */ 40 | 41 | hr { 42 | display: block; 43 | height: 1px; 44 | border: 0; 45 | border-top: 1px solid #ccc; 46 | margin: 1em 0; 47 | padding: 0; 48 | } 49 | 50 | /* 51 | * Remove the gap between audio, canvas, iframes, 52 | * images, videos and the bottom of their containers: 53 | * https://github.com/h5bp/html5-boilerplate/issues/440 54 | */ 55 | 56 | audio, 57 | canvas, 58 | iframe, 59 | img, 60 | svg, 61 | video { 62 | vertical-align: middle; 63 | } 64 | 65 | /* 66 | * Remove default fieldset styles. 67 | */ 68 | 69 | fieldset { 70 | border: 0; 71 | margin: 0; 72 | padding: 0; 73 | } 74 | 75 | /* 76 | * Allow only vertical resizing of textareas. 77 | */ 78 | 79 | textarea { 80 | resize: vertical; 81 | } 82 | 83 | /* ========================================================================== 84 | Browser Upgrade Prompt 85 | ========================================================================== */ 86 | 87 | .browserupgrade { 88 | margin: 0.2em 0; 89 | background: #ccc; 90 | color: #000; 91 | padding: 0.2em 0; 92 | } 93 | 94 | /* ========================================================================== 95 | Author's custom styles 96 | ========================================================================== */ 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | /* ========================================================================== 115 | Helper classes 116 | ========================================================================== */ 117 | 118 | /* 119 | * Hide visually and from screen readers 120 | */ 121 | 122 | .hidden { 123 | display: none !important; 124 | } 125 | 126 | /* 127 | * Hide only visually, but have it available for screen readers: 128 | * http://snook.ca/archives/html_and_css/hiding-content-for-accessibility 129 | */ 130 | 131 | .visuallyhidden { 132 | border: 0; 133 | clip: rect(0 0 0 0); 134 | height: 1px; 135 | margin: -1px; 136 | overflow: hidden; 137 | padding: 0; 138 | position: absolute; 139 | width: 1px; 140 | } 141 | 142 | /* 143 | * Extends the .visuallyhidden class to allow the element 144 | * to be focusable when navigated to via the keyboard: 145 | * https://www.drupal.org/node/897638 146 | */ 147 | 148 | .visuallyhidden.focusable:active, 149 | .visuallyhidden.focusable:focus { 150 | clip: auto; 151 | height: auto; 152 | margin: 0; 153 | overflow: visible; 154 | position: static; 155 | width: auto; 156 | } 157 | 158 | /* 159 | * Hide visually and from screen readers, but maintain layout 160 | */ 161 | 162 | .invisible { 163 | visibility: hidden; 164 | } 165 | 166 | /* 167 | * Clearfix: contain floats 168 | * 169 | * For modern browsers 170 | * 1. The space content is one way to avoid an Opera bug when the 171 | * `contenteditable` attribute is included anywhere else in the document. 172 | * Otherwise it causes space to appear at the top and bottom of elements 173 | * that receive the `clearfix` class. 174 | * 2. The use of `table` rather than `block` is only necessary if using 175 | * `:before` to contain the top-margins of child elements. 176 | */ 177 | 178 | .clearfix:before, 179 | .clearfix:after { 180 | content: " "; /* 1 */ 181 | display: table; /* 2 */ 182 | } 183 | 184 | .clearfix:after { 185 | clear: both; 186 | } 187 | 188 | /* ========================================================================== 189 | EXAMPLE Media Queries for Responsive Design. 190 | These examples override the primary ('mobile first') styles. 191 | Modify as content requires. 192 | ========================================================================== */ 193 | 194 | @media only screen and (min-width: 35em) { 195 | /* Style adjustments for viewports that meet the condition */ 196 | } 197 | 198 | @media print, 199 | (-webkit-min-device-pixel-ratio: 1.25), 200 | (min-resolution: 1.25dppx), 201 | (min-resolution: 120dpi) { 202 | /* Style adjustments for high resolution devices */ 203 | } 204 | 205 | /* ========================================================================== 206 | Print styles. 207 | Inlined to avoid the additional HTTP request: 208 | http://www.phpied.com/delay-loading-your-print-css/ 209 | ========================================================================== */ 210 | 211 | @media print { 212 | *, 213 | *:before, 214 | *:after, 215 | *:first-letter, 216 | *:first-line { 217 | background: transparent !important; 218 | color: #000 !important; /* Black prints faster: 219 | http://www.sanbeiji.com/archives/953 */ 220 | box-shadow: none !important; 221 | text-shadow: none !important; 222 | } 223 | 224 | a, 225 | a:visited { 226 | text-decoration: underline; 227 | } 228 | 229 | a[href]:after { 230 | content: " (" attr(href) ")"; 231 | } 232 | 233 | abbr[title]:after { 234 | content: " (" attr(title) ")"; 235 | } 236 | 237 | /* 238 | * Don't show links that are fragment identifiers, 239 | * or use the `javascript:` pseudo protocol 240 | */ 241 | 242 | a[href^="#"]:after, 243 | a[href^="javascript:"]:after { 244 | content: ""; 245 | } 246 | 247 | pre, 248 | blockquote { 249 | border: 1px solid #999; 250 | page-break-inside: avoid; 251 | } 252 | 253 | /* 254 | * Printing Tables: 255 | * http://css-discuss.incutio.com/wiki/Printing_Tables 256 | */ 257 | 258 | thead { 259 | display: table-header-group; 260 | } 261 | 262 | tr, 263 | img { 264 | page-break-inside: avoid; 265 | } 266 | 267 | img { 268 | max-width: 100% !important; 269 | } 270 | 271 | p, 272 | h2, 273 | h3 { 274 | orphans: 3; 275 | widows: 3; 276 | } 277 | 278 | h2, 279 | h3 { 280 | page-break-after: avoid; 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /example/css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS and IE text size adjust after device orientation change, 6 | * without disabling user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability of focused elements when they are also in an 95 | * active/hover state. 96 | */ 97 | 98 | a:active, 99 | a:hover { 100 | outline: 0; 101 | } 102 | 103 | /* Text-level semantics 104 | ========================================================================== */ 105 | 106 | /** 107 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 108 | */ 109 | 110 | abbr[title] { 111 | border-bottom: 1px dotted; 112 | } 113 | 114 | /** 115 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 116 | */ 117 | 118 | b, 119 | strong { 120 | font-weight: bold; 121 | } 122 | 123 | /** 124 | * Address styling not present in Safari and Chrome. 125 | */ 126 | 127 | dfn { 128 | font-style: italic; 129 | } 130 | 131 | /** 132 | * Address variable `h1` font-size and margin within `section` and `article` 133 | * contexts in Firefox 4+, Safari, and Chrome. 134 | */ 135 | 136 | h1 { 137 | font-size: 2em; 138 | margin: 0.67em 0; 139 | } 140 | 141 | /** 142 | * Address styling not present in IE 8/9. 143 | */ 144 | 145 | mark { 146 | background: #ff0; 147 | color: #000; 148 | } 149 | 150 | /** 151 | * Address inconsistent and variable font size in all browsers. 152 | */ 153 | 154 | small { 155 | font-size: 80%; 156 | } 157 | 158 | /** 159 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 160 | */ 161 | 162 | sub, 163 | sup { 164 | font-size: 75%; 165 | line-height: 0; 166 | position: relative; 167 | vertical-align: baseline; 168 | } 169 | 170 | sup { 171 | top: -0.5em; 172 | } 173 | 174 | sub { 175 | bottom: -0.25em; 176 | } 177 | 178 | /* Embedded content 179 | ========================================================================== */ 180 | 181 | /** 182 | * Remove border when inside `a` element in IE 8/9/10. 183 | */ 184 | 185 | img { 186 | border: 0; 187 | } 188 | 189 | /** 190 | * Correct overflow not hidden in IE 9/10/11. 191 | */ 192 | 193 | svg:not(:root) { 194 | overflow: hidden; 195 | } 196 | 197 | /* Grouping content 198 | ========================================================================== */ 199 | 200 | /** 201 | * Address margin not present in IE 8/9 and Safari. 202 | */ 203 | 204 | figure { 205 | margin: 1em 40px; 206 | } 207 | 208 | /** 209 | * Address differences between Firefox and other browsers. 210 | */ 211 | 212 | hr { 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; /* 1 */ 258 | font: inherit; /* 2 */ 259 | margin: 0; /* 3 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type="button"], /* 1 */ 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome. 354 | */ 355 | 356 | input[type="search"] { 357 | -webkit-appearance: textfield; /* 1 */ 358 | box-sizing: content-box; /* 2 */ 359 | } 360 | 361 | /** 362 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 363 | * Safari (but not Chrome) clips the cancel button when the search input has 364 | * padding (and `textfield` appearance). 365 | */ 366 | 367 | input[type="search"]::-webkit-search-cancel-button, 368 | input[type="search"]::-webkit-search-decoration { 369 | -webkit-appearance: none; 370 | } 371 | 372 | /** 373 | * Define consistent border, margin, and padding. 374 | */ 375 | 376 | fieldset { 377 | border: 1px solid #c0c0c0; 378 | margin: 0 2px; 379 | padding: 0.35em 0.625em 0.75em; 380 | } 381 | 382 | /** 383 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 384 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 385 | */ 386 | 387 | legend { 388 | border: 0; /* 1 */ 389 | padding: 0; /* 2 */ 390 | } 391 | 392 | /** 393 | * Remove default vertical scrollbar in IE 8/9/10/11. 394 | */ 395 | 396 | textarea { 397 | overflow: auto; 398 | } 399 | 400 | /** 401 | * Don't inherit the `font-weight` (applied by a rule above). 402 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 403 | */ 404 | 405 | optgroup { 406 | font-weight: bold; 407 | } 408 | 409 | /* Tables 410 | ========================================================================== */ 411 | 412 | /** 413 | * Remove most spacing between table cells. 414 | */ 415 | 416 | table { 417 | border-collapse: collapse; 418 | border-spacing: 0; 419 | } 420 | 421 | td, 422 | th { 423 | padding: 0; 424 | } 425 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /example/.htaccess: -------------------------------------------------------------------------------- 1 | # Apache Server Configs v2.14.0 | MIT License 2 | # https://github.com/h5bp/server-configs-apache 3 | 4 | # (!) Using `.htaccess` files slows down Apache, therefore, if you have 5 | # access to the main server configuration file (which is usually called 6 | # `httpd.conf`), you should add this logic there. 7 | # 8 | # https://httpd.apache.org/docs/current/howto/htaccess.html. 9 | 10 | # ###################################################################### 11 | # # CROSS-ORIGIN # 12 | # ###################################################################### 13 | 14 | # ---------------------------------------------------------------------- 15 | # | Cross-origin requests | 16 | # ---------------------------------------------------------------------- 17 | 18 | # Allow cross-origin requests. 19 | # 20 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS 21 | # http://enable-cors.org/ 22 | # http://www.w3.org/TR/cors/ 23 | 24 | # 25 | # Header set Access-Control-Allow-Origin "*" 26 | # 27 | 28 | # ---------------------------------------------------------------------- 29 | # | Cross-origin images | 30 | # ---------------------------------------------------------------------- 31 | 32 | # Send the CORS header for images when browsers request it. 33 | # 34 | # https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image 35 | # https://blog.chromium.org/2011/07/using-cross-domain-images-in-webgl-and.html 36 | 37 | 38 | 39 | 40 | SetEnvIf Origin ":" IS_CORS 41 | Header set Access-Control-Allow-Origin "*" env=IS_CORS 42 | 43 | 44 | 45 | 46 | # ---------------------------------------------------------------------- 47 | # | Cross-origin web fonts | 48 | # ---------------------------------------------------------------------- 49 | 50 | # Allow cross-origin access to web fonts. 51 | 52 | 53 | 54 | Header set Access-Control-Allow-Origin "*" 55 | 56 | 57 | 58 | # ---------------------------------------------------------------------- 59 | # | Cross-origin resource timing | 60 | # ---------------------------------------------------------------------- 61 | 62 | # Allow cross-origin access to the timing information for all resources. 63 | # 64 | # If a resource isn't served with a `Timing-Allow-Origin` header that 65 | # would allow its timing information to be shared with the document, 66 | # some of the attributes of the `PerformanceResourceTiming` object will 67 | # be set to zero. 68 | # 69 | # http://www.w3.org/TR/resource-timing/ 70 | # http://www.stevesouders.com/blog/2014/08/21/resource-timing-practical-tips/ 71 | 72 | # 73 | # Header set Timing-Allow-Origin: "*" 74 | # 75 | 76 | 77 | # ###################################################################### 78 | # # ERRORS # 79 | # ###################################################################### 80 | 81 | # ---------------------------------------------------------------------- 82 | # | Custom error messages/pages | 83 | # ---------------------------------------------------------------------- 84 | 85 | # Customize what Apache returns to the client in case of an error. 86 | # https://httpd.apache.org/docs/current/mod/core.html#errordocument 87 | 88 | ErrorDocument 404 /404.html 89 | 90 | # ---------------------------------------------------------------------- 91 | # | Error prevention | 92 | # ---------------------------------------------------------------------- 93 | 94 | # Disable the pattern matching based on filenames. 95 | # 96 | # This setting prevents Apache from returning a 404 error as the result 97 | # of a rewrite when the directory with the same name does not exist. 98 | # 99 | # https://httpd.apache.org/docs/current/content-negotiation.html#multiviews 100 | 101 | Options -MultiViews 102 | 103 | 104 | # ###################################################################### 105 | # # INTERNET EXPLORER # 106 | # ###################################################################### 107 | 108 | # ---------------------------------------------------------------------- 109 | # | Document modes | 110 | # ---------------------------------------------------------------------- 111 | 112 | # Force Internet Explorer 8/9/10 to render pages in the highest mode 113 | # available in the various cases when it may not. 114 | # 115 | # https://hsivonen.fi/doctype/#ie8 116 | # 117 | # (!) Starting with Internet Explorer 11, document modes are deprecated. 118 | # If your business still relies on older web apps and services that were 119 | # designed for older versions of Internet Explorer, you might want to 120 | # consider enabling `Enterprise Mode` throughout your company. 121 | # 122 | # https://msdn.microsoft.com/en-us/library/ie/bg182625.aspx#docmode 123 | # http://blogs.msdn.com/b/ie/archive/2014/04/02/stay-up-to-date-with-enterprise-mode-for-internet-explorer-11.aspx 124 | 125 | 126 | 127 | Header set X-UA-Compatible "IE=edge" 128 | 129 | # `mod_headers` cannot match based on the content-type, however, 130 | # the `X-UA-Compatible` response header should be send only for 131 | # HTML documents and not for the other resources. 132 | 133 | 134 | Header unset X-UA-Compatible 135 | 136 | 137 | 138 | 139 | # ---------------------------------------------------------------------- 140 | # | Iframes cookies | 141 | # ---------------------------------------------------------------------- 142 | 143 | # Allow cookies to be set from iframes in Internet Explorer. 144 | # 145 | # https://msdn.microsoft.com/en-us/library/ms537343.aspx 146 | # http://www.w3.org/TR/2000/CR-P3P-20001215/ 147 | 148 | # 149 | # Header set P3P "policyref=\"/w3c/p3p.xml\", CP=\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\"" 150 | # 151 | 152 | 153 | # ###################################################################### 154 | # # MEDIA TYPES AND CHARACTER ENCODINGS # 155 | # ###################################################################### 156 | 157 | # ---------------------------------------------------------------------- 158 | # | Media types | 159 | # ---------------------------------------------------------------------- 160 | 161 | # Serve resources with the proper media types (f.k.a. MIME types). 162 | # 163 | # https://www.iana.org/assignments/media-types/media-types.xhtml 164 | # https://httpd.apache.org/docs/current/mod/mod_mime.html#addtype 165 | 166 | 167 | 168 | # Data interchange 169 | 170 | AddType application/atom+xml atom 171 | AddType application/json json map topojson 172 | AddType application/ld+json jsonld 173 | AddType application/rss+xml rss 174 | AddType application/vnd.geo+json geojson 175 | AddType application/xml rdf xml 176 | 177 | 178 | # JavaScript 179 | 180 | # Normalize to standard type. 181 | # https://tools.ietf.org/html/rfc4329#section-7.2 182 | 183 | AddType application/javascript js 184 | 185 | 186 | # Manifest files 187 | 188 | AddType application/manifest+json webmanifest 189 | AddType application/x-web-app-manifest+json webapp 190 | AddType text/cache-manifest appcache 191 | 192 | 193 | # Media files 194 | 195 | AddType audio/mp4 f4a f4b m4a 196 | AddType audio/ogg oga ogg opus 197 | AddType image/bmp bmp 198 | AddType image/svg+xml svg svgz 199 | AddType image/webp webp 200 | AddType video/mp4 f4v f4p m4v mp4 201 | AddType video/ogg ogv 202 | AddType video/webm webm 203 | AddType video/x-flv flv 204 | 205 | # Serving `.ico` image files with a different media type 206 | # prevents Internet Explorer from displaying then as images: 207 | # https://github.com/h5bp/html5-boilerplate/commit/37b5fec090d00f38de64b591bcddcb205aadf8ee 208 | 209 | AddType image/x-icon cur ico 210 | 211 | 212 | # Web fonts 213 | 214 | AddType application/font-woff woff 215 | AddType application/font-woff2 woff2 216 | AddType application/vnd.ms-fontobject eot 217 | 218 | # Browsers usually ignore the font media types and simply sniff 219 | # the bytes to figure out the font type. 220 | # https://mimesniff.spec.whatwg.org/#matching-a-font-type-pattern 221 | # 222 | # However, Blink and WebKit based browsers will show a warning 223 | # in the console if the following font types are served with any 224 | # other media types. 225 | 226 | AddType application/x-font-ttf ttc ttf 227 | AddType font/opentype otf 228 | 229 | 230 | # Other 231 | 232 | AddType application/octet-stream safariextz 233 | AddType application/x-bb-appworld bbaw 234 | AddType application/x-chrome-extension crx 235 | AddType application/x-opera-extension oex 236 | AddType application/x-xpinstall xpi 237 | AddType text/vcard vcard vcf 238 | AddType text/vnd.rim.location.xloc xloc 239 | AddType text/vtt vtt 240 | AddType text/x-component htc 241 | 242 | 243 | 244 | # ---------------------------------------------------------------------- 245 | # | Character encodings | 246 | # ---------------------------------------------------------------------- 247 | 248 | # Serve all resources labeled as `text/html` or `text/plain` 249 | # with the media type `charset` parameter set to `UTF-8`. 250 | # 251 | # https://httpd.apache.org/docs/current/mod/core.html#adddefaultcharset 252 | 253 | AddDefaultCharset utf-8 254 | 255 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 256 | 257 | # Serve the following file types with the media type `charset` 258 | # parameter set to `UTF-8`. 259 | # 260 | # https://httpd.apache.org/docs/current/mod/mod_mime.html#addcharset 261 | 262 | 263 | AddCharset utf-8 .atom \ 264 | .bbaw \ 265 | .css \ 266 | .geojson \ 267 | .js \ 268 | .json \ 269 | .jsonld \ 270 | .manifest \ 271 | .rdf \ 272 | .rss \ 273 | .topojson \ 274 | .vtt \ 275 | .webapp \ 276 | .webmanifest \ 277 | .xloc \ 278 | .xml 279 | 280 | 281 | 282 | # ###################################################################### 283 | # # REWRITES # 284 | # ###################################################################### 285 | 286 | # ---------------------------------------------------------------------- 287 | # | Rewrite engine | 288 | # ---------------------------------------------------------------------- 289 | 290 | # (1) Turn on the rewrite engine (this is necessary in order for 291 | # the `RewriteRule` directives to work). 292 | # 293 | # https://httpd.apache.org/docs/current/mod/mod_rewrite.html#RewriteEngine 294 | # 295 | # (2) Enable the `FollowSymLinks` option if it isn't already. 296 | # 297 | # https://httpd.apache.org/docs/current/mod/core.html#options 298 | # 299 | # (3) If your web host doesn't allow the `FollowSymlinks` option, 300 | # you need to comment it out or remove it, and then uncomment 301 | # the `Options +SymLinksIfOwnerMatch` line (4), but be aware 302 | # of the performance impact. 303 | # 304 | # https://httpd.apache.org/docs/current/misc/perf-tuning.html#symlinks 305 | # 306 | # (4) Some cloud hosting services will require you set `RewriteBase`. 307 | # 308 | # https://www.rackspace.com/knowledge_center/frequently-asked-question/why-is-modrewrite-not-working-on-my-site 309 | # https://httpd.apache.org/docs/current/mod/mod_rewrite.html#rewritebase 310 | # 311 | # (5) Depending on how your server is set up, you may also need to 312 | # use the `RewriteOptions` directive to enable some options for 313 | # the rewrite engine. 314 | # 315 | # https://httpd.apache.org/docs/current/mod/mod_rewrite.html#rewriteoptions 316 | # 317 | # (6) Set %{ENV:PROTO} variable, to allow rewrites to redirect with the 318 | # appropriate schema automatically (http or https). 319 | 320 | 321 | 322 | # (1) 323 | RewriteEngine On 324 | 325 | # (2) 326 | Options +FollowSymlinks 327 | 328 | # (3) 329 | # Options +SymLinksIfOwnerMatch 330 | 331 | # (4) 332 | # RewriteBase / 333 | 334 | # (5) 335 | # RewriteOptions 336 | 337 | # (6) 338 | RewriteCond %{HTTPS} =on 339 | RewriteRule ^ - [env=proto:https] 340 | RewriteCond %{HTTPS} !=on 341 | RewriteRule ^ - [env=proto:http] 342 | 343 | 344 | 345 | # ---------------------------------------------------------------------- 346 | # | Forcing `https://` | 347 | # ---------------------------------------------------------------------- 348 | 349 | # Redirect from the `http://` to the `https://` version of the URL. 350 | # https://wiki.apache.org/httpd/RewriteHTTPToHTTPS 351 | 352 | # 353 | # RewriteEngine On 354 | # RewriteCond %{HTTPS} !=on 355 | # RewriteRule ^(.*)$ https://%{HTTP_HOST}/$1 [R=301,L] 356 | # 357 | 358 | # ---------------------------------------------------------------------- 359 | # | Suppressing / Forcing the `www.` at the beginning of URLs | 360 | # ---------------------------------------------------------------------- 361 | 362 | # The same content should never be available under two different 363 | # URLs, especially not with and without `www.` at the beginning. 364 | # This can cause SEO problems (duplicate content), and therefore, 365 | # you should choose one of the alternatives and redirect the other 366 | # one. 367 | # 368 | # By default `Option 1` (no `www.`) is activated. 369 | # http://no-www.org/faq.php?q=class_b 370 | # 371 | # If you would prefer to use `Option 2`, just comment out all the 372 | # lines from `Option 1` and uncomment the ones from `Option 2`. 373 | # 374 | # (!) NEVER USE BOTH RULES AT THE SAME TIME! 375 | 376 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 377 | 378 | # Option 1: rewrite www.example.com → example.com 379 | 380 | 381 | RewriteEngine On 382 | RewriteCond %{HTTPS} !=on 383 | RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC] 384 | RewriteRule ^ %{ENV:PROTO}://%1%{REQUEST_URI} [R=301,L] 385 | 386 | 387 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 388 | 389 | # Option 2: rewrite example.com → www.example.com 390 | # 391 | # Be aware that the following might not be a good idea if you use "real" 392 | # subdomains for certain parts of your website. 393 | 394 | # 395 | # RewriteEngine On 396 | # RewriteCond %{HTTPS} !=on 397 | # RewriteCond %{HTTP_HOST} !^www\. [NC] 398 | # RewriteCond %{SERVER_ADDR} !=127.0.0.1 399 | # RewriteCond %{SERVER_ADDR} !=::1 400 | # RewriteRule ^ %{ENV:PROTO}://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L] 401 | # 402 | 403 | 404 | # ###################################################################### 405 | # # SECURITY # 406 | # ###################################################################### 407 | 408 | # ---------------------------------------------------------------------- 409 | # | Clickjacking | 410 | # ---------------------------------------------------------------------- 411 | 412 | # Protect website against clickjacking. 413 | # 414 | # The example below sends the `X-Frame-Options` response header with 415 | # the value `DENY`, informing browsers not to display the content of 416 | # the web page in any frame. 417 | # 418 | # This might not be the best setting for everyone. You should read 419 | # about the other two possible values the `X-Frame-Options` header 420 | # field can have: `SAMEORIGIN` and `ALLOW-FROM`. 421 | # https://tools.ietf.org/html/rfc7034#section-2.1. 422 | # 423 | # Keep in mind that while you could send the `X-Frame-Options` header 424 | # for all of your website’s pages, this has the potential downside that 425 | # it forbids even non-malicious framing of your content (e.g.: when 426 | # users visit your website using a Google Image Search results page). 427 | # 428 | # Nonetheless, you should ensure that you send the `X-Frame-Options` 429 | # header for all pages that allow a user to make a state changing 430 | # operation (e.g: pages that contain one-click purchase links, checkout 431 | # or bank-transfer confirmation pages, pages that make permanent 432 | # configuration changes, etc.). 433 | # 434 | # Sending the `X-Frame-Options` header can also protect your website 435 | # against more than just clickjacking attacks: 436 | # https://cure53.de/xfo-clickjacking.pdf. 437 | # 438 | # https://tools.ietf.org/html/rfc7034 439 | # http://blogs.msdn.com/b/ieinternals/archive/2010/03/30/combating-clickjacking-with-x-frame-options.aspx 440 | # https://www.owasp.org/index.php/Clickjacking 441 | 442 | # 443 | 444 | # Header set X-Frame-Options "DENY" 445 | 446 | # # `mod_headers` cannot match based on the content-type, however, 447 | # # the `X-Frame-Options` response header should be send only for 448 | # # HTML documents and not for the other resources. 449 | 450 | # 451 | # Header unset X-Frame-Options 452 | # 453 | 454 | # 455 | 456 | # ---------------------------------------------------------------------- 457 | # | Content Security Policy (CSP) | 458 | # ---------------------------------------------------------------------- 459 | 460 | # Mitigate the risk of cross-site scripting and other content-injection 461 | # attacks. 462 | # 463 | # This can be done by setting a `Content Security Policy` which 464 | # whitelists trusted sources of content for your website. 465 | # 466 | # The example header below allows ONLY scripts that are loaded from 467 | # the current website's origin (no inline scripts, no CDN, etc). 468 | # That almost certainly won't work as-is for your website! 469 | # 470 | # To make things easier, you can use an online CSP header generator 471 | # such as: http://cspisawesome.com/. 472 | # 473 | # http://content-security-policy.com/ 474 | # http://www.html5rocks.com/en/tutorials/security/content-security-policy/ 475 | # http://www.w3.org/TR/CSP11/). 476 | 477 | # 478 | 479 | # Header set Content-Security-Policy "script-src 'self'; object-src 'self'" 480 | 481 | # # `mod_headers` cannot match based on the content-type, however, 482 | # # the `Content-Security-Policy` response header should be send 483 | # # only for HTML documents and not for the other resources. 484 | 485 | # 486 | # Header unset Content-Security-Policy 487 | # 488 | 489 | # 490 | 491 | # ---------------------------------------------------------------------- 492 | # | File access | 493 | # ---------------------------------------------------------------------- 494 | 495 | # Block access to directories without a default document. 496 | # 497 | # You should leave the following uncommented, as you shouldn't allow 498 | # anyone to surf through every directory on your server (which may 499 | # includes rather private places such as the CMS's directories). 500 | 501 | 502 | Options -Indexes 503 | 504 | 505 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 506 | 507 | # Block access to all hidden files and directories with the exception of 508 | # the visible content from within the `/.well-known/` hidden directory. 509 | # 510 | # These types of files usually contain user preferences or the preserved 511 | # state of an utility, and can include rather private places like, for 512 | # example, the `.git` or `.svn` directories. 513 | # 514 | # The `/.well-known/` directory represents the standard (RFC 5785) path 515 | # prefix for "well-known locations" (e.g.: `/.well-known/manifest.json`, 516 | # `/.well-known/keybase.txt`), and therefore, access to its visible 517 | # content should not be blocked. 518 | # 519 | # https://www.mnot.net/blog/2010/04/07/well-known 520 | # https://tools.ietf.org/html/rfc5785 521 | 522 | 523 | RewriteEngine On 524 | RewriteCond %{REQUEST_URI} "!(^|/)\.well-known/([^./]+./?)+$" [NC] 525 | RewriteCond %{SCRIPT_FILENAME} -d [OR] 526 | RewriteCond %{SCRIPT_FILENAME} -f 527 | RewriteRule "(^|/)\." - [F] 528 | 529 | 530 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 531 | 532 | # Block access to files that can expose sensitive information. 533 | # 534 | # By default, block access to backup and source files that may be 535 | # left by some text editors and can pose a security risk when anyone 536 | # has access to them. 537 | # 538 | # http://feross.org/cmsploit/ 539 | # 540 | # (!) Update the `` regular expression from below to 541 | # include any files that might end up on your production server and 542 | # can expose sensitive information about your website. These files may 543 | # include: configuration files, files that contain metadata about the 544 | # project (e.g.: project dependencies), build scripts, etc.. 545 | 546 | 547 | 548 | # Apache < 2.3 549 | 550 | Order allow,deny 551 | Deny from all 552 | Satisfy All 553 | 554 | 555 | # Apache ≥ 2.3 556 | 557 | Require all denied 558 | 559 | 560 | 561 | 562 | # ---------------------------------------------------------------------- 563 | # | HTTP Strict Transport Security (HSTS) | 564 | # ---------------------------------------------------------------------- 565 | 566 | # Force client-side SSL redirection. 567 | # 568 | # If a user types `example.com` in their browser, even if the server 569 | # redirects them to the secure version of the website, that still leaves 570 | # a window of opportunity (the initial HTTP connection) for an attacker 571 | # to downgrade or redirect the request. 572 | # 573 | # The following header ensures that browser will ONLY connect to your 574 | # server via HTTPS, regardless of what the users type in the browser's 575 | # address bar. 576 | # 577 | # (!) Remove the `includeSubDomains` optional directive if the website's 578 | # subdomains are not using HTTPS. 579 | # 580 | # http://www.html5rocks.com/en/tutorials/security/transport-layer-security/ 581 | # https://tools.ietf.org/html/draft-ietf-websec-strict-transport-sec-14#section-6.1 582 | # http://blogs.msdn.com/b/ieinternals/archive/2014/08/18/hsts-strict-transport-security-attacks-mitigations-deployment-https.aspx 583 | 584 | # 585 | # Header always set Strict-Transport-Security "max-age=16070400; includeSubDomains" 586 | # 587 | 588 | # ---------------------------------------------------------------------- 589 | # | Reducing MIME type security risks | 590 | # ---------------------------------------------------------------------- 591 | 592 | # Prevent some browsers from MIME-sniffing the response. 593 | # 594 | # This reduces exposure to drive-by download attacks and cross-origin 595 | # data leaks, and should be left uncommented, especially if the server 596 | # is serving user-uploaded content or content that could potentially be 597 | # treated as executable by the browser. 598 | # 599 | # http://www.slideshare.net/hasegawayosuke/owasp-hasegawa 600 | # http://blogs.msdn.com/b/ie/archive/2008/07/02/ie8-security-part-v-comprehensive-protection.aspx 601 | # https://msdn.microsoft.com/en-us/library/ie/gg622941.aspx 602 | # https://mimesniff.spec.whatwg.org/ 603 | 604 | 605 | Header set X-Content-Type-Options "nosniff" 606 | 607 | 608 | # ---------------------------------------------------------------------- 609 | # | Reflected Cross-Site Scripting (XSS) attacks | 610 | # ---------------------------------------------------------------------- 611 | 612 | # (1) Try to re-enable the cross-site scripting (XSS) filter built 613 | # into most web browsers. 614 | # 615 | # The filter is usually enabled by default, but in some cases it 616 | # may be disabled by the user. However, in Internet Explorer for 617 | # example, it can be re-enabled just by sending the 618 | # `X-XSS-Protection` header with the value of `1`. 619 | # 620 | # (2) Prevent web browsers from rendering the web page if a potential 621 | # reflected (a.k.a non-persistent) XSS attack is detected by the 622 | # filter. 623 | # 624 | # By default, if the filter is enabled and browsers detect a 625 | # reflected XSS attack, they will attempt to block the attack 626 | # by making the smallest possible modifications to the returned 627 | # web page. 628 | # 629 | # Unfortunately, in some browsers (e.g.: Internet Explorer), 630 | # this default behavior may allow the XSS filter to be exploited, 631 | # thereby, it's better to inform browsers to prevent the rendering 632 | # of the page altogether, instead of attempting to modify it. 633 | # 634 | # https://hackademix.net/2009/11/21/ies-xss-filter-creates-xss-vulnerabilities 635 | # 636 | # (!) Do not rely on the XSS filter to prevent XSS attacks! Ensure that 637 | # you are taking all possible measures to prevent XSS attacks, the 638 | # most obvious being: validating and sanitizing your website's inputs. 639 | # 640 | # http://blogs.msdn.com/b/ie/archive/2008/07/02/ie8-security-part-iv-the-xss-filter.aspx 641 | # http://blogs.msdn.com/b/ieinternals/archive/2011/01/31/controlling-the-internet-explorer-xss-filter-with-the-x-xss-protection-http-header.aspx 642 | # https://www.owasp.org/index.php/Cross-site_Scripting_%28XSS%29 643 | 644 | # 645 | 646 | # # (1) (2) 647 | # Header set X-XSS-Protection "1; mode=block" 648 | 649 | # # `mod_headers` cannot match based on the content-type, however, 650 | # # the `X-XSS-Protection` response header should be send only for 651 | # # HTML documents and not for the other resources. 652 | 653 | # 654 | # Header unset X-XSS-Protection 655 | # 656 | 657 | # 658 | 659 | # ---------------------------------------------------------------------- 660 | # | Server-side technology information | 661 | # ---------------------------------------------------------------------- 662 | 663 | # Remove the `X-Powered-By` response header that: 664 | # 665 | # * is set by some frameworks and server-side languages 666 | # (e.g.: ASP.NET, PHP), and its value contains information 667 | # about them (e.g.: their name, version number) 668 | # 669 | # * doesn't provide any value as far as users are concern, 670 | # and in some cases, the information provided by it can 671 | # be used by attackers 672 | # 673 | # (!) If you can, you should disable the `X-Powered-By` header from the 674 | # language / framework level (e.g.: for PHP, you can do that by setting 675 | # `expose_php = off` in `php.ini`) 676 | # 677 | # https://php.net/manual/en/ini.core.php#ini.expose-php 678 | 679 | 680 | Header unset X-Powered-By 681 | 682 | 683 | # ---------------------------------------------------------------------- 684 | # | Server software information | 685 | # ---------------------------------------------------------------------- 686 | 687 | # Prevent Apache from adding a trailing footer line containing 688 | # information about the server to the server-generated documents 689 | # (e.g.: error messages, directory listings, etc.) 690 | # 691 | # https://httpd.apache.org/docs/current/mod/core.html#serversignature 692 | 693 | ServerSignature Off 694 | 695 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 696 | 697 | # Prevent Apache from sending in the `Server` response header its 698 | # exact version number, the description of the generic OS-type or 699 | # information about its compiled-in modules. 700 | # 701 | # (!) The `ServerTokens` directive will only work in the main server 702 | # configuration file, so don't try to enable it in the `.htaccess` file! 703 | # 704 | # https://httpd.apache.org/docs/current/mod/core.html#servertokens 705 | 706 | #ServerTokens Prod 707 | 708 | 709 | # ###################################################################### 710 | # # WEB PERFORMANCE # 711 | # ###################################################################### 712 | 713 | # ---------------------------------------------------------------------- 714 | # | Compression | 715 | # ---------------------------------------------------------------------- 716 | 717 | 718 | 719 | # Force compression for mangled `Accept-Encoding` request headers 720 | # https://developer.yahoo.com/blogs/ydn/pushing-beyond-gzipping-25601.html 721 | 722 | 723 | 724 | SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding 725 | RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding 726 | 727 | 728 | 729 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 730 | 731 | # Compress all output labeled with one of the following media types. 732 | # 733 | # (!) For Apache versions below version 2.3.7 you don't need to 734 | # enable `mod_filter` and can remove the `` 735 | # and `` lines as `AddOutputFilterByType` is still in 736 | # the core directives. 737 | # 738 | # https://httpd.apache.org/docs/current/mod/mod_filter.html#addoutputfilterbytype 739 | 740 | 741 | AddOutputFilterByType DEFLATE "application/atom+xml" \ 742 | "application/javascript" \ 743 | "application/json" \ 744 | "application/ld+json" \ 745 | "application/manifest+json" \ 746 | "application/rdf+xml" \ 747 | "application/rss+xml" \ 748 | "application/schema+json" \ 749 | "application/vnd.geo+json" \ 750 | "application/vnd.ms-fontobject" \ 751 | "application/x-font-ttf" \ 752 | "application/x-javascript" \ 753 | "application/x-web-app-manifest+json" \ 754 | "application/xhtml+xml" \ 755 | "application/xml" \ 756 | "font/eot" \ 757 | "font/opentype" \ 758 | "image/bmp" \ 759 | "image/svg+xml" \ 760 | "image/vnd.microsoft.icon" \ 761 | "image/x-icon" \ 762 | "text/cache-manifest" \ 763 | "text/css" \ 764 | "text/html" \ 765 | "text/javascript" \ 766 | "text/plain" \ 767 | "text/vcard" \ 768 | "text/vnd.rim.location.xloc" \ 769 | "text/vtt" \ 770 | "text/x-component" \ 771 | "text/x-cross-domain-policy" \ 772 | "text/xml" 773 | 774 | 775 | 776 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 777 | 778 | # Map the following filename extensions to the specified 779 | # encoding type in order to make Apache serve the file types 780 | # with the appropriate `Content-Encoding` response header 781 | # (do note that this will NOT make Apache compress them!). 782 | # 783 | # If these files types would be served without an appropriate 784 | # `Content-Enable` response header, client applications (e.g.: 785 | # browsers) wouldn't know that they first need to uncompress 786 | # the response, and thus, wouldn't be able to understand the 787 | # content. 788 | # 789 | # https://httpd.apache.org/docs/current/mod/mod_mime.html#addencoding 790 | 791 | 792 | AddEncoding gzip svgz 793 | 794 | 795 | 796 | 797 | # ---------------------------------------------------------------------- 798 | # | Content transformation | 799 | # ---------------------------------------------------------------------- 800 | 801 | # Prevent intermediate caches or proxies (e.g.: such as the ones 802 | # used by mobile network providers) from modifying the website's 803 | # content. 804 | # 805 | # https://tools.ietf.org/html/rfc2616#section-14.9.5 806 | # 807 | # (!) If you are using `mod_pagespeed`, please note that setting 808 | # the `Cache-Control: no-transform` response header will prevent 809 | # `PageSpeed` from rewriting `HTML` files, and, if the 810 | # `ModPagespeedDisableRewriteOnNoTransform` directive isn't set 811 | # to `off`, also from rewriting other resources. 812 | # 813 | # https://developers.google.com/speed/pagespeed/module/configuration#notransform 814 | 815 | # 816 | # Header merge Cache-Control "no-transform" 817 | # 818 | 819 | # ---------------------------------------------------------------------- 820 | # | ETags | 821 | # ---------------------------------------------------------------------- 822 | 823 | # Remove `ETags` as resources are sent with far-future expires headers. 824 | # 825 | # https://developer.yahoo.com/performance/rules.html#etags 826 | # https://tools.ietf.org/html/rfc7232#section-2.3 827 | 828 | # `FileETag None` doesn't work in all cases. 829 | 830 | Header unset ETag 831 | 832 | 833 | FileETag None 834 | 835 | # ---------------------------------------------------------------------- 836 | # | Expires headers | 837 | # ---------------------------------------------------------------------- 838 | 839 | # Serve resources with far-future expires headers. 840 | # 841 | # (!) If you don't control versioning with filename-based 842 | # cache busting, you should consider lowering the cache times 843 | # to something like one week. 844 | # 845 | # https://httpd.apache.org/docs/current/mod/mod_expires.html 846 | 847 | 848 | 849 | ExpiresActive on 850 | ExpiresDefault "access plus 1 month" 851 | 852 | # CSS 853 | 854 | ExpiresByType text/css "access plus 1 year" 855 | 856 | 857 | # Data interchange 858 | 859 | ExpiresByType application/atom+xml "access plus 1 hour" 860 | ExpiresByType application/rdf+xml "access plus 1 hour" 861 | ExpiresByType application/rss+xml "access plus 1 hour" 862 | 863 | ExpiresByType application/json "access plus 0 seconds" 864 | ExpiresByType application/ld+json "access plus 0 seconds" 865 | ExpiresByType application/schema+json "access plus 0 seconds" 866 | ExpiresByType application/vnd.geo+json "access plus 0 seconds" 867 | ExpiresByType application/xml "access plus 0 seconds" 868 | ExpiresByType text/xml "access plus 0 seconds" 869 | 870 | 871 | # Favicon (cannot be renamed!) and cursor images 872 | 873 | ExpiresByType image/vnd.microsoft.icon "access plus 1 week" 874 | ExpiresByType image/x-icon "access plus 1 week" 875 | 876 | # HTML 877 | 878 | ExpiresByType text/html "access plus 0 seconds" 879 | 880 | 881 | # JavaScript 882 | 883 | ExpiresByType application/javascript "access plus 1 year" 884 | ExpiresByType application/x-javascript "access plus 1 year" 885 | ExpiresByType text/javascript "access plus 1 year" 886 | 887 | 888 | # Manifest files 889 | 890 | ExpiresByType application/manifest+json "access plus 1 week" 891 | ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds" 892 | ExpiresByType text/cache-manifest "access plus 0 seconds" 893 | 894 | 895 | # Media files 896 | 897 | ExpiresByType audio/ogg "access plus 1 month" 898 | ExpiresByType image/bmp "access plus 1 month" 899 | ExpiresByType image/gif "access plus 1 month" 900 | ExpiresByType image/jpeg "access plus 1 month" 901 | ExpiresByType image/png "access plus 1 month" 902 | ExpiresByType image/svg+xml "access plus 1 month" 903 | ExpiresByType image/webp "access plus 1 month" 904 | ExpiresByType video/mp4 "access plus 1 month" 905 | ExpiresByType video/ogg "access plus 1 month" 906 | ExpiresByType video/webm "access plus 1 month" 907 | 908 | 909 | # Web fonts 910 | 911 | # Embedded OpenType (EOT) 912 | ExpiresByType application/vnd.ms-fontobject "access plus 1 month" 913 | ExpiresByType font/eot "access plus 1 month" 914 | 915 | # OpenType 916 | ExpiresByType font/opentype "access plus 1 month" 917 | 918 | # TrueType 919 | ExpiresByType application/x-font-ttf "access plus 1 month" 920 | 921 | # Web Open Font Format (WOFF) 1.0 922 | ExpiresByType application/font-woff "access plus 1 month" 923 | ExpiresByType application/x-font-woff "access plus 1 month" 924 | ExpiresByType font/woff "access plus 1 month" 925 | 926 | # Web Open Font Format (WOFF) 2.0 927 | ExpiresByType application/font-woff2 "access plus 1 month" 928 | 929 | 930 | # Other 931 | 932 | ExpiresByType text/x-cross-domain-policy "access plus 1 week" 933 | 934 | 935 | 936 | # ---------------------------------------------------------------------- 937 | # | File concatenation | 938 | # ---------------------------------------------------------------------- 939 | 940 | # Allow concatenation from within specific files. 941 | # 942 | # e.g.: 943 | # 944 | # If you have the following lines in a file called, for 945 | # example, `main.combined.js`: 946 | # 947 | # 948 | # 949 | # 950 | # Apache will replace those lines with the content of the 951 | # specified files. 952 | 953 | # 954 | # 955 | # Options +Includes 956 | # AddOutputFilterByType INCLUDES application/javascript \ 957 | # application/x-javascript \ 958 | # text/javascript 959 | # SetOutputFilter INCLUDES 960 | # 961 | # 962 | # Options +Includes 963 | # AddOutputFilterByType INCLUDES text/css 964 | # SetOutputFilter INCLUDES 965 | # 966 | # 967 | 968 | # ---------------------------------------------------------------------- 969 | # | Filename-based cache busting | 970 | # ---------------------------------------------------------------------- 971 | 972 | # If you're not using a build process to manage your filename version 973 | # revving, you might want to consider enabling the following directives 974 | # to route all requests such as `/style.12345.css` to `/style.css`. 975 | # 976 | # To understand why this is important and even a better solution than 977 | # using something like `*.css?v231`, please see: 978 | # http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/ 979 | 980 | # 981 | # RewriteEngine On 982 | # RewriteCond %{REQUEST_FILENAME} !-f 983 | # RewriteRule ^(.+)\.(\d+)\.(bmp|css|cur|gif|ico|jpe?g|js|png|svgz?|webp|webmanifest)$ $1.$3 [L] 984 | # 985 | --------------------------------------------------------------------------------