├── .babelrc ├── .codeclimate.yml ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── docs ├── 00 Documentation │ └── Documentation.md └── index.md ├── examples ├── 00 Connecting Targets │ ├── Multiple Targets │ │ ├── index.js │ │ ├── target-container.js │ │ └── target.js │ └── Single Target │ │ ├── index.js │ │ ├── target-container.js │ │ └── target.js ├── 01 Styling │ └── Themes │ │ ├── body.js │ │ ├── index.js │ │ ├── target-container.js │ │ ├── target.js │ │ └── themes.js ├── 02 Dynamic Menus │ ├── Dynamic Menus │ │ ├── body.js │ │ ├── index.js │ │ ├── target-container.js │ │ └── target.js │ └── Stress Test │ │ ├── body.js │ │ ├── index.js │ │ ├── target-container.js │ │ └── target.js └── README.md ├── package.json ├── react-menus.md ├── scripts ├── build.sh ├── buildSiteIndexPages.sh ├── buildStaticSite.sh ├── cssTransformLoader.js ├── markdownLoader.js ├── prism.js ├── publishStaticSite.sh ├── resolvers.js └── startSiteDevServer.sh ├── site ├── Constants.js ├── IndexPage.js ├── LICENSE ├── base.less ├── client.js ├── components │ ├── CodeBlock.js │ ├── CodeBlock.less │ ├── Cover.js │ ├── Cover.less │ ├── Header.js │ ├── Header.less │ ├── NavBar.js │ ├── NavBar.less │ ├── PageBody.js │ ├── PageBody.less │ ├── SideBar.js │ ├── SideBar.less │ └── StaticHTMLBlock.js ├── constants.less ├── pages │ ├── APIPage.js │ ├── ExamplePage.js │ └── HomePage.js ├── renderPath.js ├── webpack-client.config.js └── webpack-prerender.config.js └── src ├── context_menu.js └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env","react","stage-0"], 3 | "plugins": ["transform-class-properties","add-module-exports"] 4 | } 5 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | duplication: 4 | enabled: true 5 | config: 6 | languages: 7 | - ruby 8 | - javascript 9 | - python 10 | - php 11 | eslint: 12 | enabled: true 13 | fixme: 14 | enabled: true 15 | ratings: 16 | paths: 17 | - "**.inc" 18 | - "**.js" 19 | - "**.jsx" 20 | - "**.module" 21 | - "**.php" 22 | - "**.py" 23 | - "**.rb" 24 | exclude_paths: 25 | - site/ 26 | - scripts/ 27 | - examples/ 28 | - docs/ 29 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | __site__ 2 | __site_prerender__ 3 | site 4 | scripts 5 | dist 6 | lib 7 | **/node_modules 8 | **/webpack*.config.js 9 | tests.webpack.js 10 | karma.conf.js 11 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | ecmaFeatures: 2 | modules: true 3 | jsx: true 4 | 5 | env: 6 | amd: true 7 | browser: true 8 | es6: true 9 | jquery: true 10 | node: true 11 | 12 | # http://eslint.org/docs/rules/ 13 | rules: 14 | # Possible Errors 15 | comma-dangle: [2, never] 16 | no-cond-assign: 2 17 | no-console: 0 18 | no-constant-condition: 2 19 | no-control-regex: 2 20 | no-debugger: 2 21 | no-dupe-args: 2 22 | no-dupe-keys: 2 23 | no-duplicate-case: 2 24 | no-empty: 2 25 | no-empty-character-class: 2 26 | no-ex-assign: 2 27 | no-extra-boolean-cast: 2 28 | no-extra-parens: 0 29 | no-extra-semi: 2 30 | no-func-assign: 2 31 | no-inner-declarations: [2, functions] 32 | no-invalid-regexp: 2 33 | no-irregular-whitespace: 2 34 | no-negated-in-lhs: 2 35 | no-obj-calls: 2 36 | no-regex-spaces: 2 37 | no-sparse-arrays: 2 38 | no-unexpected-multiline: 2 39 | no-unreachable: 2 40 | use-isnan: 2 41 | valid-jsdoc: 0 42 | valid-typeof: 2 43 | 44 | # Best Practices 45 | accessor-pairs: 2 46 | block-scoped-var: 0 47 | complexity: [2, 6] 48 | consistent-return: 0 49 | curly: 0 50 | default-case: 0 51 | dot-location: 0 52 | dot-notation: 0 53 | eqeqeq: 2 54 | guard-for-in: 2 55 | no-alert: 2 56 | no-caller: 2 57 | no-case-declarations: 2 58 | no-div-regex: 2 59 | no-else-return: 0 60 | no-empty-label: 2 61 | no-empty-pattern: 2 62 | no-eq-null: 2 63 | no-eval: 2 64 | no-extend-native: 2 65 | no-extra-bind: 2 66 | no-fallthrough: 2 67 | no-floating-decimal: 0 68 | no-implicit-coercion: 0 69 | no-implied-eval: 2 70 | no-invalid-this: 0 71 | no-iterator: 2 72 | no-labels: 0 73 | no-lone-blocks: 2 74 | no-loop-func: 2 75 | no-magic-number: 0 76 | no-multi-spaces: 0 77 | no-multi-str: 0 78 | no-native-reassign: 2 79 | no-new-func: 2 80 | no-new-wrappers: 2 81 | no-new: 2 82 | no-octal-escape: 2 83 | no-octal: 2 84 | no-proto: 2 85 | no-redeclare: 2 86 | no-return-assign: 2 87 | no-script-url: 2 88 | no-self-compare: 2 89 | no-sequences: 0 90 | no-throw-literal: 0 91 | no-unused-expressions: 2 92 | no-useless-call: 2 93 | no-useless-concat: 2 94 | no-void: 2 95 | no-warning-comments: 0 96 | no-with: 2 97 | radix: 2 98 | vars-on-top: 0 99 | wrap-iife: 2 100 | yoda: 0 101 | 102 | # Strict 103 | strict: 0 104 | 105 | # Variables 106 | init-declarations: 0 107 | no-catch-shadow: 2 108 | no-delete-var: 2 109 | no-label-var: 2 110 | no-shadow-restricted-names: 2 111 | no-shadow: 0 112 | no-undef-init: 2 113 | no-undef: 0 114 | no-undefined: 0 115 | no-unused-vars: 0 116 | no-use-before-define: 0 117 | 118 | # Node.js and CommonJS 119 | callback-return: 2 120 | global-require: 2 121 | handle-callback-err: 2 122 | no-mixed-requires: 0 123 | no-new-require: 0 124 | no-path-concat: 2 125 | no-process-exit: 2 126 | no-restricted-modules: 0 127 | no-sync: 0 128 | 129 | # Stylistic Issues 130 | array-bracket-spacing: 0 131 | block-spacing: 0 132 | brace-style: 0 133 | camelcase: 0 134 | comma-spacing: 0 135 | comma-style: 0 136 | computed-property-spacing: 0 137 | consistent-this: 0 138 | eol-last: 0 139 | func-names: 0 140 | func-style: 0 141 | id-length: 0 142 | id-match: 0 143 | indent: 0 144 | jsx-quotes: 0 145 | key-spacing: 0 146 | linebreak-style: 0 147 | lines-around-comment: 0 148 | max-depth: 0 149 | max-len: 0 150 | max-nested-callbacks: 0 151 | max-params: 0 152 | max-statements: [2, 30] 153 | new-cap: 0 154 | new-parens: 0 155 | newline-after-var: 0 156 | no-array-constructor: 0 157 | no-bitwise: 0 158 | no-continue: 0 159 | no-inline-comments: 0 160 | no-lonely-if: 0 161 | no-mixed-spaces-and-tabs: 0 162 | no-multiple-empty-lines: 0 163 | no-negated-condition: 0 164 | no-nested-ternary: 0 165 | no-new-object: 0 166 | no-plusplus: 0 167 | no-restricted-syntax: 0 168 | no-spaced-func: 0 169 | no-ternary: 0 170 | no-trailing-spaces: 0 171 | no-underscore-dangle: 0 172 | no-unneeded-ternary: 0 173 | object-curly-spacing: 0 174 | one-var: 0 175 | operator-assignment: 0 176 | operator-linebreak: 0 177 | padded-blocks: 0 178 | quote-props: 0 179 | quotes: 0 180 | require-jsdoc: 0 181 | semi-spacing: 0 182 | semi: 0 183 | sort-vars: 0 184 | space-after-keywords: 0 185 | space-before-blocks: 0 186 | space-before-function-paren: 0 187 | space-before-keywords: 0 188 | space-in-parens: 0 189 | space-infix-ops: 0 190 | space-return-throw-case: 0 191 | space-unary-ops: 0 192 | spaced-comment: 0 193 | wrap-regex: 0 194 | 195 | # ECMAScript 6 196 | arrow-body-style: 0 197 | arrow-parens: 0 198 | arrow-spacing: 0 199 | constructor-super: 0 200 | generator-star-spacing: 0 201 | no-arrow-condition: 0 202 | no-class-assign: 0 203 | no-const-assign: 0 204 | no-dupe-class-members: 0 205 | no-this-before-super: 0 206 | no-var: 0 207 | object-shorthand: 0 208 | prefer-arrow-callback: 0 209 | prefer-const: 0 210 | prefer-reflect: 0 211 | prefer-spread: 0 212 | prefer-template: 0 213 | require-yield: 0 214 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | lib 36 | __site* 37 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | scripts 3 | examples 4 | site 5 | __site__ 6 | __site_prerender__ 7 | webpack.config.js 8 | .babelrc 9 | .eslintrc 10 | bower.json 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Joshua Christman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://badge.fury.io/js/react-context-menus.svg)](https://www.npmjs.com/package/react-context-menus) 2 | [![Dependency Status](https://david-dm.org/jchristman/react-context-menus.svg)](https://david-dm.org/jchristman/react-context-menus) 3 | [![devDependency Status](https://david-dm.org/jchristman/react-context-menus/dev-status.svg)](https://david-dm.org/jchristman/react-context-menus#info=devDependencies) 4 | [![peerDependency Status](https://david-dm.org/jchristman/react-context-menus/peer-status.svg)](https://david-dm.org/jchristman/react-context-menus#info=peerDependencies) 5 | [![Code Climate](https://codeclimate.com/github/jchristman/react-context-menus/badges/gpa.svg)](https://codeclimate.com/github/jchristman/react-context-menus) 6 | 7 | # react-context-menus 8 | A library to make context menus very easy for React. See the demo and documentation at http://jchristman.github.io/react-context-menus/ 9 | -------------------------------------------------------------------------------- /docs/00 Documentation/Documentation.md: -------------------------------------------------------------------------------- 1 | Overview 2 | ======== 3 | 4 | React Context Menus is a library that aims to simplify the process of getting right click menus into your application. After searching for a long time, I was unsatisfied with the libraries that existed. The aim with this library is to simplify the API while providing control over the functionality and display of the menu to the developer. It is built on top of [Zippy UI react-menus](https://github.com/zippyui/react-menus). 5 | 6 | Installation 7 | ============ 8 | 9 | As easy as it gets: 10 | 11 | ```bash 12 | npm install --save react-context-menus 13 | ``` 14 | 15 | Usage 16 | ===== 17 | 18 | This library is meant to be used as a Container. It will provide a prop to your component called ```connectContextMenu```. You can call this function with any part of your component to make it right clickable. Like so: 19 | 20 | ------------------- 21 | ```js 22 | var React = require('react'); 23 | var ContextMenu = require('react-context-menus'); 24 | 25 | var ComponentToReceiveContextMenu = React.createClass({ 26 | render: function() { 27 | return this.props.connectContextMenu( 28 |
29 | ); 30 | } 31 | }); 32 | 33 | var menu_items = [ 34 | /* ... */ 35 | ]; 36 | 37 | module.exports = ContextMenu(menu_items)(ComponentToReceiveContextMenu); 38 | ``` 39 | ------------------- 40 | ```js 41 | import React from 'react'; 42 | import ContextMenu from 'react-context-menus'; 43 | 44 | class ComponentToReceiveContextMenu extends React.Component { 45 | render() { 46 | return this.props.connectContextMenu( 47 |
48 | ); 49 | } 50 | }); 51 | 52 | const menu_items = [ 53 | /* ... */ 54 | ]; 55 | 56 | export default ContextMenu(menu_items)(ComponentToReceiveContextMenu); 57 | ``` 58 | ------------------- 59 | ```js 60 | import React from 'react'; 61 | import ContextMenu from 'react-context-menus'; 62 | 63 | @ContextMenu(menu_items) 64 | export default class ComponentToReceiveContextMenu extends React.Component { 65 | render() { 66 | return this.props.connectContextMenu( 67 |
68 | ); 69 | } 70 | } 71 | ``` 72 | ------------------- 73 | 74 | Arguments To ContextMenu 75 | ======================== 76 | 77 | The ContextMenu container can receive two arguments - a menu_items and an options argument. The menu_items argument must be an array or a function that receives the props of the item being wrapped and returns an array. The options argument must be an object or a function that receives the props of the item being wrapped and returns an object. 78 | 79 | Menu items are defined below. See [react-menus](https://github.com/zippyui/react-menus) for more info. 80 | 81 | ```js 82 | // This can be either a static array or a function to build a dynamic menu based on the thing that is clicked. 83 | const menu_items = (props) => { 84 | return [ 85 | { 86 | label: 'Label 1' // The actual text that gets displayed 87 | onClick: (event, props) => {} // A function that gets called on click of menu item. It receives the props of the wrapped component. 88 | }, 89 | '-', 90 | { 91 | label: 'Label 2', 92 | disabled: true // Disable the item. Defaults to false. 93 | } 94 | ]; 95 | } 96 | ``` 97 | 98 | Options are defined as such: 99 | 100 | ```js 101 | // This can be either a static object or a function to build a dynamic object based on the thing that is clicked. 102 | const options = (props) => { 103 | return { 104 | theme: props.theme.theme, // The theme object is applied to items 105 | style: props.theme.style, // The style object is applied to the overall menu 106 | at: { x: 30, y: 30 }, // The at object positions the menu (if you want to do it manually) 107 | show: props.show // The show property will determine whether to show the menu (if you want to do it manually) 108 | container: { zIndex: 1 } // The container property will adjust the menu container (the fixed element that's attached to your component). 109 | // Current, the only option is to adjust its zIndex if needed. 110 | } 111 | } 112 | ``` 113 | 114 | How It Works 115 | ============ 116 | 117 | The library works by appending a child to the wrapped component that will be the context menu container. Then, any time the "connected" DOM items are right clicked, it will calculate the position of the menu and render it to the DOM at the context menu container. The menu is added and removed in an intelligent way, such that it does not exist when it is not showing. 118 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | React Context Menu 2 | ================== 3 | 4 | React Context Menus is a library that aims to simplify the process of getting right click menus into your application. After searching for a long time, I was unsatisfied with the libraries that existed. The aim with this library is to simplify the API while providing control over the functionality and display of the menu to the developer. It is built on top of [Zippy UI react-menus](https://github.com/zippyui/react-menus). 5 | -------------------------------------------------------------------------------- /examples/00 Connecting Targets/Multiple Targets/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import Target from './target-container'; 4 | 5 | export default class SingleTargetApp extends Component { 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | render() { 11 | return ( 12 |
13 |

14 | Browse the Source 15 |

16 |

17 | In this example, there are multiple context menu "spots" on the same component, each of which generates the same context menu. 18 |

19 | 20 |
21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/00 Connecting Targets/Multiple Targets/target-container.js: -------------------------------------------------------------------------------- 1 | import ContextMenu from 'react-context-menus'; 2 | import Target from './target.js'; 3 | 4 | const menu_items = [ 5 | { 6 | label: 'Item 1', 7 | onClick: () => alert('Clicked Item 1!') 8 | }, 9 | { 10 | label: 'Item 2', 11 | onClick: () => alert('Clicked Item 2!') 12 | }, 13 | '-', 14 | { 15 | label: 'Item 3', 16 | onClick: () => alert('Clicked Item 3 below the divider!') 17 | } 18 | ]; 19 | 20 | const options = { 21 | theme: { 22 | style: { 23 | color: 'black' 24 | } 25 | } 26 | } 27 | 28 | export default ContextMenu(menu_items, options)(Target); 29 | -------------------------------------------------------------------------------- /examples/00 Connecting Targets/Multiple Targets/target.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const style = { 4 | position: 'relative', 5 | width: 300, 6 | height: 300, 7 | backgroundColor: '#111', 8 | color: 'white', 9 | textAlign: 'center' 10 | } 11 | 12 | const textStyle = { 13 | pointerEvents: 'none', 14 | pointer: 'cursor' 15 | } 16 | 17 | const context_menu_styles = [ 18 | { 19 | position: 'absolute', 20 | width: 50, 21 | heigh: 50, 22 | top: 40, 23 | left: 20, 24 | backgroundColor: 'red', 25 | }, 26 | { 27 | position: 'absolute', 28 | width: 50, 29 | heigh: 50, 30 | top: 120, 31 | right: 40, 32 | backgroundColor: 'blue' 33 | }, 34 | { 35 | position: 'absolute', 36 | width: 50, 37 | heigh: 50, 38 | bottom: 60, 39 | left: 140, 40 | backgroundColor: 'green' 41 | } 42 | ]; 43 | 44 | const Target = (props) => { 45 | return ( 46 |
47 | { 48 | context_menu_styles.map((box, idx) => { 49 | return props.connectContextMenu( 50 |
51 |
Right click here
52 |
53 | ) 54 | }) 55 | } 56 |
57 | ); 58 | } 59 | 60 | export default Target; 61 | -------------------------------------------------------------------------------- /examples/00 Connecting Targets/Single Target/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import Target from './target-container'; 4 | 5 | export default class SingleTargetApp extends Component { 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | render() { 11 | return ( 12 |
13 |

14 | Browse the Source 15 |

16 |

17 | This is a simplest example that exists. Right click in the box below to get a context menu. 18 |

19 | 20 |
21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/00 Connecting Targets/Single Target/target-container.js: -------------------------------------------------------------------------------- 1 | import ContextMenu from 'react-context-menus'; 2 | import Target from './target.js'; 3 | 4 | const menu_items = [ 5 | { 6 | label: 'Item 1', 7 | onClick: (event, props, item) => alert('Clicked Item 1!') 8 | }, 9 | { 10 | label: 'Item 2', 11 | onClick: () => alert('Clicked Item 2!') 12 | }, 13 | '-', 14 | { 15 | label: 'Item 3', 16 | onClick: () => alert('Clicked Item 3 below the divider!') 17 | } 18 | ]; 19 | 20 | const options = { 21 | theme: { 22 | style: { 23 | color: 'black' 24 | } 25 | } 26 | } 27 | 28 | export default ContextMenu(menu_items, options)(Target); 29 | -------------------------------------------------------------------------------- /examples/00 Connecting Targets/Single Target/target.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const style = { 4 | position: 'relative', 5 | width: 300, 6 | height: 300, 7 | backgroundColor: '#111', 8 | color: 'white', 9 | textAlign: 'center' 10 | } 11 | 12 | const textStyle = { 13 | pointerEvents: 'none', 14 | pointer: 'cursor' 15 | } 16 | 17 | const Target = (props) => { 18 | return props.connectContextMenu( 19 |
20 |
21 | Right click here for a context menu 22 |
23 |
24 | ); 25 | } 26 | 27 | export default Target; 28 | -------------------------------------------------------------------------------- /examples/01 Styling/Themes/body.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'underscore'; 3 | import Target from './target-container.js'; 4 | 5 | import * as _Themes from './themes.js'; 6 | const Themes = _.omit(_Themes, '__esModule'); // Because it gets added by default 7 | 8 | class Body extends React.Component { 9 | constructor() { 10 | super(); 11 | this.state = { 12 | selected: Object.keys(Themes)[0] 13 | } 14 | } 15 | 16 | handleChange(event) { 17 | this.setState({ selected: event.target.value }); 18 | } 19 | 20 | render() { 21 | return ( 22 |
23 | 32 | 33 | 34 |
35 | ); 36 | } 37 | } 38 | 39 | export default Body; 40 | -------------------------------------------------------------------------------- /examples/01 Styling/Themes/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import Body from './body.js'; 4 | 5 | export default class SingleTargetApp extends Component { 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | render() { 11 | return ( 12 |
13 |

14 | Browse the Source 15 |

16 |

17 | This example shows how you can change the style of the context menus. The menu will automatically open when you change the selection to demonstrate the colors. You can still right click around though... See the react-menus homepage for full stying information. 18 |

19 | 20 |
21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/01 Styling/Themes/target-container.js: -------------------------------------------------------------------------------- 1 | import ContextMenu from 'react-context-menus'; 2 | import Target from './target.js'; 3 | 4 | const menu_items = [ 5 | { 6 | label: 'Item 1', 7 | onClick: (event, props, item) => alert('Clicked Item 1!') 8 | }, 9 | { 10 | label: 'Item 2', 11 | onClick: () => alert('Clicked Item 2!') 12 | }, 13 | '-', 14 | { 15 | label: 'Item 3', 16 | onClick: () => alert('Clicked Item 3 below the divider!') 17 | } 18 | ]; 19 | 20 | const options = (props) => { 21 | return { 22 | theme: props.theme.theme, 23 | style: props.theme.style, 24 | container: { 25 | zIndex: 1 26 | } 27 | } 28 | } 29 | 30 | export default ContextMenu(menu_items, options)(Target); 31 | -------------------------------------------------------------------------------- /examples/01 Styling/Themes/target.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const style = { 4 | position: 'relative', 5 | width: 300, 6 | height: 300, 7 | backgroundColor: '#111', 8 | color: 'white', 9 | textAlign: 'center' 10 | } 11 | 12 | const textStyle = { 13 | pointerEvents: 'none', 14 | pointer: 'cursor' 15 | } 16 | 17 | const Target = (props) => { 18 | return props.connectContextMenu( 19 |
20 |
21 | Right click here for a context menu 22 |
23 |
24 | ); 25 | } 26 | 27 | export default Target; 28 | -------------------------------------------------------------------------------- /examples/01 Styling/Themes/themes.js: -------------------------------------------------------------------------------- 1 | export const AURORA_RED = { 2 | name: 'Aurora Red', 3 | style: { border: '1px solid #e7ada9' }, 4 | theme: { 5 | style: { background: '#b93a32', color: 'white' }, 6 | overStyle: { background: '#f3d6d4', color: 'black' }, 7 | activeStyle: { background: '#b93a32', color: 'white' } 8 | } 9 | } 10 | 11 | export const SERENITY = { 12 | name: 'Serenity', 13 | style: { border: '1px solid #6181bd' }, 14 | theme: { 15 | style: { background: '#92a8d1', color: 'black' }, 16 | overStyle: { background: '#e9eef6', color: 'black' }, 17 | activeStyle: { background: '#92a8d1', color: 'black' } 18 | } 19 | } 20 | 21 | export const LUSH_MEADOW = { 22 | name: 'Lush Meadow', 23 | style: { border: '1px solid #e7fff9' }, 24 | theme: { 25 | style: { background: '#006e51', color: 'white' }, 26 | overStyle: { background: '#afffea', color: 'black' }, 27 | activeStyle: { background: '#006e51', color: 'white' } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/02 Dynamic Menus/Dynamic Menus/body.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Target from './target-container.js'; 4 | 5 | const items = [ 6 | 'Box 1', 7 | 'Box 2', 8 | 'Box 3', 9 | 'Box 4', 10 | 'Box 5', 11 | 'Box 6' 12 | ]; 13 | 14 | const Body = (props) => ( 15 |
16 | { items.map((item, idx) => (
{item}
)) } 17 |
18 | ) 19 | 20 | export default Body; 21 | -------------------------------------------------------------------------------- /examples/02 Dynamic Menus/Dynamic Menus/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import Body from './body'; 4 | 5 | export default class SingleTargetApp extends Component { 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | render() { 11 | return ( 12 |
13 |

14 | Browse the Source 15 |

16 |

17 | This example shows how you can use a single function to generate unique menus based on the props of the item that was right clicked. 18 |

19 | 20 |
21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/02 Dynamic Menus/Dynamic Menus/target-container.js: -------------------------------------------------------------------------------- 1 | import ContextMenu from 'react-context-menus'; 2 | import Target from './target.js'; 3 | 4 | const menu_items = (props) => { 5 | return [ 6 | { 7 | label: 'Item 1, ' + props.text, 8 | onClick: (event, props, item) => alert('Clicked Item 1, ' + props.text + '!') 9 | }, 10 | { 11 | label: 'Item 2, ' + props.text + '', 12 | onClick: () => alert('Clicked Item 2, ' + props.text + '!') 13 | }, 14 | '-', 15 | { 16 | label: 'Item 3, ' + props.text + '', 17 | onClick: () => alert('Clicked Item 3, ' + props.text + ' below the divider!') 18 | } 19 | ]; 20 | } 21 | 22 | const options = { 23 | theme: { 24 | style: { 25 | color: 'black' 26 | } 27 | } 28 | } 29 | 30 | export default ContextMenu(menu_items, options)(Target); 31 | -------------------------------------------------------------------------------- /examples/02 Dynamic Menus/Dynamic Menus/target.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const style = { 4 | position: 'relative', 5 | width: 200, 6 | height: 40, 7 | backgroundColor: '#111', 8 | color: 'white', 9 | textAlign: 'center', 10 | marginBottom: 5 11 | } 12 | 13 | const textStyle = { 14 | pointerEvents: 'none', 15 | pointer: 'cursor' 16 | } 17 | 18 | const Target = (props) => { 19 | return props.connectContextMenu( 20 |
21 |
22 | { props.children } 23 |
24 |
25 | ); 26 | } 27 | 28 | export default Target; 29 | -------------------------------------------------------------------------------- /examples/02 Dynamic Menus/Stress Test/body.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Target from './target-container.js'; 4 | 5 | const items = [ 6 | 'Box 1', 7 | 'Box 2', 8 | 'Box 3', 9 | 'Box 4', 10 | 'Box 5', 11 | 'Box 6' 12 | ]; 13 | 14 | const Body = (props) => ( 15 |
16 | { items.map((item, idx) => (
{item}
)) } 17 |
18 | ) 19 | 20 | export default Body; 21 | -------------------------------------------------------------------------------- /examples/02 Dynamic Menus/Stress Test/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import Body from './body'; 4 | 5 | export default class SingleTargetApp extends Component { 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | render() { 11 | return ( 12 |
13 |

14 | Browse the Source 15 |

16 |

17 | This example not yet built. 18 |

19 |
20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/02 Dynamic Menus/Stress Test/target-container.js: -------------------------------------------------------------------------------- 1 | import ContextMenu from 'react-context-menus'; 2 | import Target from './target.js'; 3 | 4 | const menu_items = (props) => { 5 | return [ 6 | { 7 | label: 'Item 1, ' + props.text, 8 | onClick: (event, props, item) => alert('Clicked Item 1, ' + props.text + '!') 9 | }, 10 | { 11 | label: 'Item 2, ' + props.text + '', 12 | onClick: () => alert('Clicked Item 2, ' + props.text + '!') 13 | }, 14 | '-', 15 | { 16 | label: 'Item 3, ' + props.text + '', 17 | onClick: () => alert('Clicked Item 3, ' + props.text + ' below the divider!') 18 | } 19 | ]; 20 | } 21 | 22 | const options = { 23 | theme: { 24 | style: { 25 | color: 'black' 26 | } 27 | } 28 | } 29 | 30 | export default ContextMenu(menu_items, options)(Target); 31 | -------------------------------------------------------------------------------- /examples/02 Dynamic Menus/Stress Test/target.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const style = { 4 | position: 'relative', 5 | width: 200, 6 | height: 40, 7 | backgroundColor: '#111', 8 | color: 'white', 9 | textAlign: 'center', 10 | marginBottom: 5 11 | } 12 | 13 | const textStyle = { 14 | pointerEvents: 'none', 15 | pointer: 'cursor' 16 | } 17 | 18 | const Target = (props) => { 19 | return props.connectContextMenu( 20 |
21 |
22 | { props.children } 23 |
24 |
25 | ); 26 | } 27 | 28 | export default Target; 29 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchristman/react-context-menus/8bf5fb7b0aeb740345d07e7128007384ee6bcf5c/examples/README.md -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-context-menus", 3 | "version": "1.0.10", 4 | "description": "Better context menus for React", 5 | "main": "lib/context_menu.js", 6 | "scripts": { 7 | "start": "./scripts/startSiteDevServer.sh", 8 | "build-site": "./scripts/buildStaticSite.sh", 9 | "publish-site": "./scripts/publishStaticSite.sh", 10 | "build": "./scripts/build.sh", 11 | "lint": "eslint .", 12 | "prepublish": "npm run build" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/jchristman/react-context-menus.git" 17 | }, 18 | "keywords": [ 19 | "javascript", 20 | "react", 21 | "reactjs", 22 | "contextmenu", 23 | "context-menu", 24 | "react-component", 25 | "rightclick", 26 | "right-click" 27 | ], 28 | "author": "Joshua Christman (http://github.com/jchristman)", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/jchristman/react-context-menus/issues" 32 | }, 33 | "homepage": "https://github.com/jchristman/react-context-menus", 34 | "devDependencies": { 35 | "babel-cli": "^6.9.0", 36 | "babel-core": "6.x.x", 37 | "babel-loader": "^7.1.2", 38 | "babel-plugin-add-module-exports": "^0.2.1", 39 | "babel-plugin-transform-class-properties": "^6.9.0", 40 | "babel-preset-env": "^1.6.0", 41 | "babel-preset-react": "6.x.x", 42 | "babel-preset-stage-0": "^6.24.1", 43 | "css-loader": "^0.28.7", 44 | "extract-text-webpack-plugin": "^3.0.0", 45 | "html-loader": "^0.5.1", 46 | "less": "^2.7.1", 47 | "less-loader": "^4.0.5", 48 | "marked": "^0.3.6", 49 | "null-loader": "^0.1.1", 50 | "react-hot-loader": "^1.3.0", 51 | "style-loader": "^0.18.2", 52 | "webpack": "^3.6.0", 53 | "webpack-dev-server": "^2.4.1" 54 | }, 55 | "dependencies": { 56 | "invariant": "2.x.x", 57 | "react": ">=0.14.x", 58 | "react-dom": ">=0.14.x", 59 | "react-menus2": "3.0.0", 60 | "underscore": "^1.8.3" 61 | }, 62 | "peerDependencies": { 63 | "invariant": "2.x.x", 64 | "react": ">=0.14.x", 65 | "react-dom": ">=0.14.x", 66 | "react-menus2": "3.0.0", 67 | "underscore": "^1.8.3" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /react-menus.md: -------------------------------------------------------------------------------- 1 | react-menus 2 | =========== 3 | 4 | > A carefully crafted menu for React 5 | 6 | ## Install 7 | 8 | ```sh 9 | $ npm install react-menus --save 10 | ``` 11 | 12 | ## Description 13 | 14 | The `react-menus` component is a context-menu like widget for React. It features **smart positioning**, overflow **scrolling** on too many menu items and **smart submenu positioning**. 15 | 16 | ## Changelog 17 | 18 | See [Changelog](./CHANGELOG.md) 19 | 20 | ## Roadmap 21 | 22 | See [Roadmap](./ROADMAP.md) 23 | 24 | ## Usage 25 | 26 | ```jsx 27 | var items = [ 28 | { 29 | label: 'hello', 30 | onClick: function(itemProps, index, event) { 31 | console.log('well, hello') 32 | } 33 | }, 34 | '-', //show separator 35 | { 36 | label: 'hi' 37 | }, 38 | { 39 | label: 'export', 40 | disabled: true 41 | } 42 | ] 43 | 44 | function onClick(event, props, index){ 45 | 46 | } 47 | 48 | 49 | ``` 50 | 51 | For rendering separators, just use a `'-'` in the items array. 52 | 53 | ## Properties 54 | 55 | * items: Object[] 56 | * onClick: Function(event, props, index) - Called on a click on a direct menu item. For clicks on menu items at any level of nesting, use `onChildClick` 57 | * onChildClick: Function(event, props) - Called when a menu item at any level of nesting was clicked 58 | * columns: String[] - defaults to ['label'] 59 | 60 | For every item in the items property, a row will be rendered, with all the columns specified in `props.columns`. Every column displays the value in item[<column_name>]. 61 | 62 | Every item can optionally have an **onClick** property, which is called when the item is clicked. (**onClick: Function(event, itemProps, index)**). Making an item disabled is done by specifying **disabled: true** on the item object. 63 | 64 | * expander: String/ReactElement - an expander tool to use when a menu item has other subitems. Defaults to the unicode arrow character **›**. 65 | 66 | ### Styling & advanced usage 67 | 68 | By default, the `react-menus` component comes with built-in structural styles as well as with styles for a default nice theme. The specified theme is applied to menu items. If you don't want to render menu items with the default theme, just specify `theme=''` (or any falsy value). 69 | 70 | ```jsx 71 | var items = [ {label: 'Save', onClick: function(){} }, { label: 'Export'}] 72 | 73 | ``` 74 | 75 | Or you can specify your own theme for the button. The value for the `theme` property is just an object with different styles: 76 | 77 | ```jsx 78 | var theme = { 79 | style: { background: 'blue'}, 80 | overStyle: { background: 'red', color: 'white'}, 81 | activeStyle: { background: 'magenta'}, 82 | expandedStyle: { background: 'magenta'}, 83 | disabledStyle: {background: 'gray'} 84 | } 85 | 86 | 87 | ``` 88 | 89 | Or you can specify a theme as string: 'default'. The `'default'` theme is the only one built in. 90 | 91 | ```jsx 92 | 93 | ``` 94 | But you can add named themes: 95 | ```jsx 96 | var theme = require('react-menus').theme 97 | 98 | theme.goldenTheme = { overStyle: {background: 'yellow'}} 99 | 100 | 101 | ``` 102 | 103 | For styling menu separators, set the desired style properties on `Menu.Separator.style` 104 | 105 | ```jsx 106 | var Menu = require('react-menus') 107 | 108 | var Separator = Menu.Separator 109 | 110 | Separator.style = { 111 | background: 'red' //the color of the separator 112 | } 113 | 114 | Separator.size = 10 //will be 10 px in height 115 | ``` 116 | 117 | ### Style props 118 | 119 | Styling menu items overrides theme styles. 120 | 121 | * itemStyle - style to be applied to menu items. Overrides `theme.style` 122 | * itemOverStyle - style to be applied to menu items on mouse over. Overrides `theme.overStyle` 123 | * itemActiveStyle - style to be applied to menu items on mouse down on the item. Overrides `theme.activeStyle` 124 | * itemExpandedStyle - style to be applied to menu items when the item is expanded. Overrides `theme.expandedStyle` 125 | * itemDisabledStyle - style to be applied to disabled menu items. Overrides `theme.defaultStyle` 126 | 127 | * cellStyle - style to be applied to menu item cells (expect the expander cell). 128 | 129 | ### Scrolling 130 | 131 | Menu scrolling is enabled by default (`enableScroll: true`). When you have too many items, and the menu is bigger than it's parent container, the menu shows a scrolling user interface. 132 | 133 | Or you can specify a `maxHeight` property on the menu, and if that is exceeded, the menu is scrollable. 134 | 135 | ```jsx 136 | 137 | ``` 138 | 139 | Of course you can turn off scrolling with `enableScroll: false` 140 | 141 | ### Smart submenus 142 | 143 | Showing and hiding submenus is implemented with a smart algorithm, as described [here](http://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown). Also submenu positioning is made taking into account the available space. More documentation on this soon. 144 | 145 | ## License 146 | 147 | ```MIT``` 148 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # build minified standalone version in dist 4 | rm -rf dist 5 | ./node_modules/.bin/webpack --output-filename=dist/ContextMenu.js 6 | ./node_modules/.bin/webpack --output-filename=dist/ContextMenu.min.js --optimize-minimize 7 | 8 | # build ES5 modules to lib 9 | rm -rf lib 10 | babel src --out-dir lib 11 | -------------------------------------------------------------------------------- /scripts/buildSiteIndexPages.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('babel-core/register'); 3 | 4 | // -*- mode: js -*- 5 | "use strict"; 6 | 7 | var fs = require('fs'); 8 | var path = require('path'); 9 | var glob = require('glob'); 10 | var Constants = require('../site/Constants'); 11 | var renderPath = require('../__site_prerender__/renderPath'); 12 | var flatten = require('lodash/flatten'); 13 | 14 | var sitePath = path.join(__dirname, '../__site__'); 15 | if (!fs.existsSync(sitePath)) { 16 | fs.mkdirSync(sitePath); 17 | } 18 | 19 | var files = { 20 | 'main.css': 'main.css', 21 | 'main.js': 'main.js' 22 | }; 23 | 24 | if (process.env.NODE_ENV === 'production') { 25 | Object.keys(files).forEach(function(fileName) { 26 | var searchPath = path.join( 27 | __dirname, 28 | '../__site__/' + fileName.replace('.', '-*.') 29 | ); 30 | var hashedFilename = glob.sync(searchPath)[0]; 31 | if (!hashedFilename) { 32 | throw new Error( 33 | 'Hashed file of "' + fileName + '" ' + 34 | 'not found when searching with "' + searchPath + '"' 35 | ); 36 | } 37 | 38 | files[fileName] = path.basename(hashedFilename); 39 | }); 40 | } 41 | 42 | var locations = flatten([ 43 | Constants.APIPages.map(function (group) { 44 | return group.pages; 45 | }), 46 | Constants.ExamplePages.map(function (group) { 47 | return group.pages; 48 | }), 49 | ]).reduce(function(paths, pages) { 50 | return paths.concat( 51 | Object.keys(pages).map(function(key) { 52 | return pages[key].location; 53 | }) 54 | ); 55 | }, []); 56 | 57 | locations.forEach(function(fileName) { 58 | var props = { 59 | location: fileName, 60 | devMode: process.env.NODE_ENV !== 'production', 61 | files: files 62 | }; 63 | 64 | renderPath(fileName, props, function(content) { 65 | fs.writeFileSync( 66 | path.join(sitePath, fileName), 67 | content 68 | ); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /scripts/buildStaticSite.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | PATH=$(npm bin):$PATH 4 | 5 | rm -rf ./__site__ 6 | rm -rf ./__site_prerender__ 7 | NODE_ENV=production webpack --config "$PWD/site/webpack-client.config.js" 8 | NODE_ENV=production webpack --config "$PWD/site/webpack-prerender.config.js" 9 | NODE_ENV=production ./scripts/buildSiteIndexPages.sh 10 | -------------------------------------------------------------------------------- /scripts/cssTransformLoader.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var postcss = require('postcss'); 4 | var autoPrefixer = require('autoprefixer'); 5 | 6 | module.exports = function(content) { 7 | if (this && this.cacheable) { 8 | // Webpack specific call 9 | this.cacheable(); 10 | } 11 | 12 | content = postcss() 13 | .use(autoPrefixer()) 14 | .process(content).css; 15 | 16 | return content; 17 | }; 18 | -------------------------------------------------------------------------------- /scripts/markdownLoader.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var marked = require('marked'); 4 | var prism = require('./prism'); 5 | 6 | // functions come before keywords 7 | prism.languages.insertBefore('javascript', 'keyword', { 8 | 'var': /\b(this)\b/g, 9 | 'block-keyword': /\b(if|else|while|for|function)\b/g, 10 | 'primitive': /\b(true|false|null|undefined)\b/g, 11 | 'function': prism.languages.function, 12 | }); 13 | 14 | prism.languages.insertBefore('javascript', { 15 | 'qualifier': /\b[A-Z][a-z0-9_]+/g, 16 | }); 17 | 18 | marked.setOptions({ 19 | xhtml: true, 20 | highlight: function(code) { 21 | return prism.highlight(code, prism.languages.javascript); 22 | } 23 | }); 24 | 25 | var renderer = new marked.Renderer(); 26 | 27 | renderer.heading = function (text, level) { 28 | var escapedText = text.toLowerCase().replace(/[^\w]+/g, '-'); 29 | 30 | // A hack to have proper anchor scrolling despite the navbar on top of them. 31 | // In CSS, they'll be positioned relatively. 32 | return '' + text + ' '; 33 | }; 34 | 35 | renderer.code = function(code, lang, escaped) { 36 | if (this.options.highlight) { 37 | var out = this.options.highlight(code, lang); 38 | if (out != null && out !== code) { 39 | escaped = true; 40 | code = out; 41 | } 42 | } 43 | return '' + 44 | (escaped ? code : escapeCode(code, true)) + 45 | ''; 46 | }; 47 | 48 | function escapeCode(code) { 49 | return code 50 | .replace(/&/g, '&') 51 | .replace(//g, '>') 53 | .replace(/"/g, '"') 54 | .replace(/'/g, '''); 55 | } 56 | 57 | module.exports = function(markdown) { 58 | if (this && this.cacheable) { 59 | // Webpack specific call 60 | this.cacheable(); 61 | } 62 | 63 | return marked(markdown, { renderer: renderer }); 64 | }; 65 | -------------------------------------------------------------------------------- /scripts/prism.js: -------------------------------------------------------------------------------- 1 | /* ********************************************** 2 | Begin prism-core.js 3 | ********************************************** */ 4 | 5 | self = (typeof window !== 'undefined') 6 | ? window // if in browser 7 | : ( 8 | (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope) 9 | ? self // if in worker 10 | : {} // if in node js 11 | ); 12 | 13 | /** 14 | * Prism: Lightweight, robust, elegant syntax highlighting 15 | * MIT license http://www.opensource.org/licenses/mit-license.php/ 16 | * @author Lea Verou http://lea.verou.me 17 | */ 18 | 19 | var Prism = (function(){ 20 | 21 | // Private helper vars 22 | var lang = /\blang(?:uage)?-(?!\*)(\w+)\b/i; 23 | 24 | var _ = self.Prism = { 25 | util: { 26 | encode: function (tokens) { 27 | if (tokens instanceof Token) { 28 | return new Token(tokens.type, _.util.encode(tokens.content), tokens.alias); 29 | } else if (_.util.type(tokens) === 'Array') { 30 | return tokens.map(_.util.encode); 31 | } else { 32 | return tokens.replace(/&/g, '&').replace(/ text.length) { 271 | // Something went terribly wrong, ABORT, ABORT! 272 | break tokenloop; 273 | } 274 | 275 | if (str instanceof Token) { 276 | continue; 277 | } 278 | 279 | pattern.lastIndex = 0; 280 | 281 | var match = pattern.exec(str); 282 | 283 | if (match) { 284 | if(lookbehind) { 285 | lookbehindLength = match[1].length; 286 | } 287 | 288 | var from = match.index - 1 + lookbehindLength, 289 | match = match[0].slice(lookbehindLength), 290 | len = match.length, 291 | to = from + len, 292 | before = str.slice(0, from + 1), 293 | after = str.slice(to + 1); 294 | 295 | var args = [i, 1]; 296 | 297 | if (before) { 298 | args.push(before); 299 | } 300 | 301 | var wrapped = new Token(token, inside? _.tokenize(match, inside) : match, alias); 302 | 303 | args.push(wrapped); 304 | 305 | if (after) { 306 | args.push(after); 307 | } 308 | 309 | Array.prototype.splice.apply(strarr, args); 310 | } 311 | } 312 | } 313 | } 314 | 315 | return strarr; 316 | }, 317 | 318 | hooks: { 319 | all: {}, 320 | 321 | add: function (name, callback) { 322 | var hooks = _.hooks.all; 323 | 324 | hooks[name] = hooks[name] || []; 325 | 326 | hooks[name].push(callback); 327 | }, 328 | 329 | run: function (name, env) { 330 | var callbacks = _.hooks.all[name]; 331 | 332 | if (!callbacks || !callbacks.length) { 333 | return; 334 | } 335 | 336 | for (var i=0, callback; callback = callbacks[i++];) { 337 | callback(env); 338 | } 339 | } 340 | } 341 | }; 342 | 343 | var Token = _.Token = function(type, content, alias) { 344 | this.type = type; 345 | this.content = content; 346 | this.alias = alias; 347 | }; 348 | 349 | Token.stringify = function(o, language, parent) { 350 | if (typeof o == 'string') { 351 | return o; 352 | } 353 | 354 | if (Object.prototype.toString.call(o) == '[object Array]') { 355 | return o.map(function(element) { 356 | return Token.stringify(element, language, o); 357 | }).join(''); 358 | } 359 | 360 | var env = { 361 | type: o.type, 362 | content: Token.stringify(o.content, language, parent), 363 | tag: 'span', 364 | classes: ['token', o.type], 365 | attributes: {}, 366 | language: language, 367 | parent: parent 368 | }; 369 | 370 | if (env.type == 'comment') { 371 | env.attributes['spellcheck'] = 'true'; 372 | } 373 | 374 | if (o.alias) { 375 | var aliases = _.util.type(o.alias) === 'Array' ? o.alias : [o.alias]; 376 | Array.prototype.push.apply(env.classes, aliases); 377 | } 378 | 379 | _.hooks.run('wrap', env); 380 | 381 | var attributes = ''; 382 | 383 | for (var name in env.attributes) { 384 | attributes += name + '="' + (env.attributes[name] || '') + '"'; 385 | } 386 | 387 | return '<' + env.tag + ' class="' + env.classes.join(' ') + '" ' + attributes + '>' + env.content + ''; 388 | 389 | }; 390 | 391 | if (!self.document) { 392 | if (!self.addEventListener) { 393 | // in Node.js 394 | return self.Prism; 395 | } 396 | // In worker 397 | self.addEventListener('message', function(evt) { 398 | var message = JSON.parse(evt.data), 399 | lang = message.language, 400 | code = message.code; 401 | 402 | self.postMessage(JSON.stringify(_.util.encode(_.tokenize(code, _.languages[lang])))); 403 | self.close(); 404 | }, false); 405 | 406 | return self.Prism; 407 | } 408 | 409 | // Get current script and highlight 410 | var script = document.getElementsByTagName('script'); 411 | 412 | script = script[script.length - 1]; 413 | 414 | if (script) { 415 | _.filename = script.src; 416 | 417 | if (document.addEventListener && !script.hasAttribute('data-manual')) { 418 | document.addEventListener('DOMContentLoaded', _.highlightAll); 419 | } 420 | } 421 | 422 | return self.Prism; 423 | 424 | })(); 425 | 426 | if (typeof module !== 'undefined' && module.exports) { 427 | module.exports = Prism; 428 | } 429 | 430 | 431 | /* ********************************************** 432 | Begin prism-markup.js 433 | ********************************************** */ 434 | 435 | Prism.languages.markup = { 436 | 'comment': //g, 437 | 'prolog': /<\?.+?\?>/, 438 | 'doctype': //, 439 | 'cdata': //i, 440 | 'tag': { 441 | pattern: /<\/?[\w:-]+\s*(?:\s+[\w:-]+(?:=(?:("|')(\\?[\w\W])*?\1|[^\s'">=]+))?\s*)*\/?>/gi, 442 | inside: { 443 | 'tag': { 444 | pattern: /^<\/?[\w:-]+/i, 445 | inside: { 446 | 'punctuation': /^<\/?/, 447 | 'namespace': /^[\w-]+?:/ 448 | } 449 | }, 450 | 'attr-value': { 451 | pattern: /=(?:('|")[\w\W]*?(\1)|[^\s>]+)/gi, 452 | inside: { 453 | 'punctuation': /=|>|"/g 454 | } 455 | }, 456 | 'punctuation': /\/?>/g, 457 | 'attr-name': { 458 | pattern: /[\w:-]+/g, 459 | inside: { 460 | 'namespace': /^[\w-]+?:/ 461 | } 462 | } 463 | 464 | } 465 | }, 466 | 'entity': /\&#?[\da-z]{1,8};/gi 467 | }; 468 | 469 | // Plugin to make entity title show the real entity, idea by Roman Komarov 470 | Prism.hooks.add('wrap', function(env) { 471 | 472 | if (env.type === 'entity') { 473 | env.attributes['title'] = env.content.replace(/&/, '&'); 474 | } 475 | }); 476 | 477 | 478 | /* ********************************************** 479 | Begin prism-css.js 480 | ********************************************** */ 481 | 482 | Prism.languages.css = { 483 | 'comment': /\/\*[\w\W]*?\*\//g, 484 | 'atrule': { 485 | pattern: /@[\w-]+?.*?(;|(?=\s*{))/gi, 486 | inside: { 487 | 'punctuation': /[;:]/g 488 | } 489 | }, 490 | 'url': /url\((["']?).*?\1\)/gi, 491 | 'selector': /[^\{\}\s][^\{\};]*(?=\s*\{)/g, 492 | 'property': /(\b|\B)[\w-]+(?=\s*:)/ig, 493 | 'string': /("|')(\\?.)*?\1/g, 494 | 'important': /\B!important\b/gi, 495 | 'punctuation': /[\{\};:]/g, 496 | 'function': /[-a-z0-9]+(?=\()/ig 497 | }; 498 | 499 | if (Prism.languages.markup) { 500 | Prism.languages.insertBefore('markup', 'tag', { 501 | 'style': { 502 | pattern: /[\w\W]*?<\/style>/ig, 503 | inside: { 504 | 'tag': { 505 | pattern: /|<\/style>/ig, 506 | inside: Prism.languages.markup.tag.inside 507 | }, 508 | rest: Prism.languages.css 509 | }, 510 | alias: 'language-css' 511 | } 512 | }); 513 | 514 | Prism.languages.insertBefore('inside', 'attr-value', { 515 | 'style-attr': { 516 | pattern: /\s*style=("|').+?\1/ig, 517 | inside: { 518 | 'attr-name': { 519 | pattern: /^\s*style/ig, 520 | inside: Prism.languages.markup.tag.inside 521 | }, 522 | 'punctuation': /^\s*=\s*['"]|['"]\s*$/, 523 | 'attr-value': { 524 | pattern: /.+/gi, 525 | inside: Prism.languages.css 526 | } 527 | }, 528 | alias: 'language-css' 529 | } 530 | }, Prism.languages.markup.tag); 531 | } 532 | 533 | /* ********************************************** 534 | Begin prism-clike.js 535 | ********************************************** */ 536 | 537 | Prism.languages.clike = { 538 | 'comment': [ 539 | { 540 | pattern: /(^|[^\\])\/\*[\w\W]*?\*\//g, 541 | lookbehind: true 542 | }, 543 | { 544 | pattern: /(^|[^\\:])\/\/.*?(\r?\n|$)/g, 545 | lookbehind: true 546 | } 547 | ], 548 | 'string': /("|')(\\?.)*?\1/g, 549 | 'class-name': { 550 | pattern: /((?:(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/ig, 551 | lookbehind: true, 552 | inside: { 553 | punctuation: /(\.|\\)/ 554 | } 555 | }, 556 | 'keyword': /\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/g, 557 | 'boolean': /\b(true|false)\b/g, 558 | 'function': { 559 | pattern: /[a-z0-9_]+\(/ig, 560 | inside: { 561 | punctuation: /\(/ 562 | } 563 | }, 564 | 'number': /\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?)\b/g, 565 | 'operator': /[-+]{1,2}|!|<=?|>=?|={1,3}|&{1,2}|\|?\||\?|\*|\/|\~|\^|\%/g, 566 | 'ignore': /&(lt|gt|amp);/gi, 567 | 'punctuation': /[{}[\];(),.:]/g 568 | }; 569 | 570 | 571 | /* ********************************************** 572 | Begin prism-javascript.js 573 | ********************************************** */ 574 | 575 | Prism.languages.javascript = Prism.languages.extend('clike', { 576 | 'keyword': /\b(break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|false|finally|for|function|get|if|implements|import|in|instanceof|interface|let|new|null|package|private|protected|public|return|set|static|super|switch|this|throw|true|try|typeof|var|void|while|with|yield)\b/g, 577 | 'number': /\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?|NaN|-?Infinity)\b/g 578 | }); 579 | 580 | Prism.languages.insertBefore('javascript', 'keyword', { 581 | 'regex': { 582 | pattern: /(^|[^/])\/(?!\/)(\[.+?]|\\.|[^/\r\n])+\/[gim]{0,3}(?=\s*($|[\r\n,.;})]))/g, 583 | lookbehind: true 584 | } 585 | }); 586 | 587 | if (Prism.languages.markup) { 588 | Prism.languages.insertBefore('markup', 'tag', { 589 | 'script': { 590 | pattern: /[\w\W]*?<\/script>/ig, 591 | inside: { 592 | 'tag': { 593 | pattern: /|<\/script>/ig, 594 | inside: Prism.languages.markup.tag.inside 595 | }, 596 | rest: Prism.languages.javascript 597 | }, 598 | alias: 'language-javascript' 599 | } 600 | }); 601 | } 602 | 603 | 604 | /* ********************************************** 605 | Begin prism-file-highlight.js 606 | ********************************************** */ 607 | 608 | (function(){ 609 | 610 | if (!self.Prism || !self.document || !document.querySelector) { 611 | return; 612 | } 613 | 614 | var Extensions = { 615 | 'js': 'javascript', 616 | 'html': 'markup', 617 | 'svg': 'markup', 618 | 'xml': 'markup', 619 | 'py': 'python', 620 | 'rb': 'ruby' 621 | }; 622 | 623 | Array.prototype.slice.call(document.querySelectorAll('pre[data-src]')).forEach(function(pre) { 624 | var src = pre.getAttribute('data-src'); 625 | var extension = (src.match(/\.(\w+)$/) || [,''])[1]; 626 | var language = Extensions[extension] || extension; 627 | 628 | var code = document.createElement('code'); 629 | code.className = 'language-' + language; 630 | 631 | pre.textContent = ''; 632 | 633 | code.textContent = 'Loading…'; 634 | 635 | pre.appendChild(code); 636 | 637 | var xhr = new XMLHttpRequest(); 638 | 639 | xhr.open('GET', src, true); 640 | 641 | xhr.onreadystatechange = function() { 642 | if (xhr.readyState == 4) { 643 | 644 | if (xhr.status < 400 && xhr.responseText) { 645 | code.textContent = xhr.responseText; 646 | 647 | Prism.highlightElement(code); 648 | } 649 | else if (xhr.status >= 400) { 650 | code.textContent = '✖ Error ' + xhr.status + ' while fetching file: ' + xhr.statusText; 651 | } 652 | else { 653 | code.textContent = '✖ Error: File does not exist or is empty'; 654 | } 655 | } 656 | }; 657 | 658 | xhr.send(null); 659 | }); 660 | 661 | })(); 662 | -------------------------------------------------------------------------------- /scripts/publishStaticSite.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | PROJECT_DIRECTORY="react-context-menus" 5 | SITE_DIRECTORY="$PROJECT_DIRECTORY-site" 6 | GITHUB_REPO="https://github.com/jchristman/react-context-menus.git" 7 | GH_PAGES_SITE="http://jchristman.github.io/react-context-menus/" 8 | 9 | # Move to parent dir 10 | cd ../ 11 | 12 | # Setup repo if doesnt exist 13 | if [ ! -d "$SITE_DIRECTORY" ]; then 14 | read -p "No site repo setup, can I create it at \"$PWD/$SITE_DIRECTORY\"? [Y/n] " -r 15 | echo 16 | if [[ ! $REPLY =~ ^[Yy]$ ]] && [[ ! $REPLY == "" ]] 17 | then 18 | echo "Exit by user" 19 | exit 1 20 | fi 21 | git clone "$GITHUB_REPO" "$SITE_DIRECTORY" 22 | cd "$SITE_DIRECTORY" 23 | git branch gh-pages 24 | git checkout gh-pages 25 | git push --set-upstream origin gh-pages 26 | cd ../ 27 | fi 28 | 29 | cd "$PROJECT_DIRECTORY" 30 | npm run build-site 31 | open __site__/index.html 32 | cd ../ 33 | 34 | echo 35 | echo 36 | read -p "Are you ready to publish? [Y/n] " -r 37 | echo 38 | if [[ ! $REPLY =~ ^[Yy]$ ]] && [[ ! $REPLY == "" ]] 39 | then 40 | echo "Exit by user" 41 | exit 1 42 | fi 43 | 44 | cd "$SITE_DIRECTORY" 45 | git checkout gh-pages 46 | git reset --hard 47 | git clean -dfx 48 | git fetch 49 | git rebase 50 | rm -Rf * 51 | echo "$PWD" 52 | cp -R ../$PROJECT_DIRECTORY/__site__/* . 53 | git add --all 54 | git commit -m "Update website" 55 | git push 56 | sleep 1 57 | open $GH_PAGES_SITE 58 | -------------------------------------------------------------------------------- /scripts/resolvers.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var glob = require('glob'); 3 | var path = require('path'); 4 | 5 | function buildHasteMap() { 6 | var root = path.resolve(__dirname, '../src'); 7 | var hasteMap = {}; 8 | glob.sync(root + '/**/*.{js,css}').forEach(function(file) { 9 | var code = fs.readFileSync(file); 10 | var regex = /@providesModule ([^\s*]+)/; 11 | var result = regex.exec(code); 12 | if (result) { 13 | var id = result[1]; 14 | if (path.extname(file) === '.css') { 15 | id += '.css'; 16 | } 17 | hasteMap[id] = file; 18 | } 19 | }); 20 | return hasteMap; 21 | }; 22 | 23 | function resolveHasteDefines() { 24 | // Run in the context of webpack's compiler. 25 | var hasteMap = buildHasteMap(); 26 | this.resolvers.normal.plugin('module', function(request, callback) { 27 | var hastePath = hasteMap[request.request]; 28 | if (hastePath) { 29 | return callback(null, { 30 | path: hastePath, 31 | query: request.query, 32 | file: true, 33 | resolved: true 34 | }); 35 | } 36 | return callback(); 37 | }); 38 | } 39 | 40 | module.exports = { 41 | resolveHasteDefines: resolveHasteDefines 42 | }; 43 | -------------------------------------------------------------------------------- /scripts/startSiteDevServer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | PATH=$(npm bin):$PATH 4 | 5 | rm -rf ./__site__ 6 | rm -rf ./__site_prerender__ 7 | ./node_modules/.bin/webpack --config "$PWD/site/webpack-prerender.config.js" 8 | ./scripts/buildSiteIndexPages.sh 9 | ./node_modules/.bin/webpack-dev-server --config "$PWD/site/webpack-client.config.js" --hot --content-base __site__ 10 | -------------------------------------------------------------------------------- /site/Constants.js: -------------------------------------------------------------------------------- 1 | export const APIPages = [{ 2 | title: 'Documentation', 3 | pages: { 4 | API: { 5 | location: 'docs-api.html', 6 | title: 'API' 7 | } 8 | } 9 | } 10 | ]; 11 | 12 | export const ExamplePages = [ 13 | { 14 | title: 'Connecting Targets', 15 | pages: { 16 | SINGLE_TARGET: { 17 | location: 'index.html', 18 | title: 'Single Target' 19 | }, 20 | MULTIPLE_TARGETS: { 21 | location: 'examples-multiple-targets.html', 22 | title: 'Multiple Targets' 23 | } 24 | } 25 | }, { 26 | title: 'Styling', 27 | pages: { 28 | THEMES: { 29 | location: 'examples-themes.html', 30 | title: 'Themes' 31 | } 32 | } 33 | }, { 34 | title: 'Dynamic Menus', 35 | pages: { 36 | DYNAMIC_MENUS: { 37 | location: 'examples-dynamic-menus.html', 38 | title: 'Dynamic Menus' 39 | }, 40 | STRESS_TEST: { 41 | location: 'examples-stress-test.html', 42 | title: 'Stress Test' 43 | } 44 | } 45 | } 46 | ]; 47 | 48 | export const DOCS_DEFAULT = APIPages[0].pages.API; 49 | export const EXAMPLES_DEFAULT = ExamplePages[0].pages.SINGLE_TARGET; 50 | -------------------------------------------------------------------------------- /site/IndexPage.js: -------------------------------------------------------------------------------- 1 | import './base.less'; 2 | import Constants, { APIPages, ExamplePages } from './Constants'; 3 | import APIPage from './pages/APIPage'; 4 | import ExamplePage from './pages/ExamplePage'; 5 | import React, { Component } from 'react'; 6 | import ReactDOMServer from 'react-dom/server'; 7 | 8 | const APIDocs = { 9 | API: require('../docs/00 Documentation/Documentation.md'), 10 | }; 11 | 12 | const Examples = { 13 | SINGLE_TARGET: require('../examples/00 Connecting Targets/Single Target'), 14 | MULTIPLE_TARGETS: require('../examples/00 Connecting Targets/Multiple Targets'), 15 | THEMES: require('../examples/01 Styling/Themes'), 16 | DYNAMIC_MENUS: require('../examples/02 Dynamic Menus/Dynamic Menus'), 17 | STRESS_TEST: require('../examples/02 Dynamic Menus/Stress Test') 18 | }; 19 | 20 | export default class IndexPage extends Component { 21 | static getDoctype() { 22 | return ''; 23 | } 24 | 25 | static renderToString(props) { 26 | return IndexPage.getDoctype() + 27 | ReactDOMServer.renderToString(); 28 | } 29 | 30 | constructor(props) { 31 | super(props); 32 | this.state = { 33 | renderPage: !this.props.devMode 34 | }; 35 | } 36 | 37 | render() { 38 | // Dump out our current props to a global object via a script tag so 39 | // when initialising the browser environment we can bootstrap from the 40 | // same props as what each page was rendered with. 41 | const browserInitScriptObj = { 42 | __html: 'window.INITIAL_PROPS = ' + JSON.stringify(this.props) + ';\n' 43 | }; 44 | 45 | return ( 46 | 47 | 48 | 49 | React Context Menus 50 | 51 | 52 | 53 | 54 | 55 | {this.state.renderPage && this.renderPage()} 56 | 57 | 59 | 60 | 61 | ); 62 | } 63 | 64 | renderPage() { 65 | for (let groupIndex in APIPages) { 66 | const group = APIPages[groupIndex]; 67 | const pageKeys = Object.keys(group.pages); 68 | 69 | for (let i = 0; i < pageKeys.length; i++) { 70 | const key = pageKeys[i]; 71 | const page = group.pages[key]; 72 | 73 | if (this.props.location === page.location) { 74 | return ; 76 | } 77 | } 78 | } 79 | 80 | for (let groupIndex in ExamplePages) { 81 | const group = ExamplePages[groupIndex]; 82 | const pageKeys = Object.keys(group.pages); 83 | 84 | for (let i = 0; i < pageKeys.length; i++) { 85 | const key = pageKeys[i]; 86 | const page = group.pages[key]; 87 | const Component = Examples[key]; 88 | 89 | if (this.props.location === page.location) { 90 | return ( 91 | 92 | 93 | 94 | ); 95 | } 96 | } 97 | } 98 | 99 | throw new Error( 100 | 'Page of location ' + 101 | JSON.stringify(this.props.location) + 102 | ' not found.' 103 | ); 104 | } 105 | 106 | componentDidMount() { 107 | if (!this.state.renderPage) { 108 | this.setState({ 109 | renderPage: true 110 | }); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /site/LICENSE: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | Copyright (c) 2015, Facebook, Inc. All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | * Neither the name Facebook nor the names of its contributors may be used to 16 | endorse or promote products derived from this software without specific 17 | prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 23 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /site/base.less: -------------------------------------------------------------------------------- 1 | @import './constants.less'; 2 | 3 | * { box-sizing: border-box; } 4 | 5 | html, body { 6 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 7 | -webkit-text-size-adjust: 100%; 8 | -ms-text-size-adjust: 100%; 9 | margin: 0; 10 | padding: 0; 11 | -webkit-font-smoothing: antialiased; 12 | text-rendering: optimizeLegibility; 13 | color: @body-color; 14 | font-family: 'Helvetica Neue', Helvetica, sans-serif; 15 | font-size: 14px; 16 | line-height: 1.625; 17 | } 18 | 19 | @media only screen and (min-width: @screen-tablet) { 20 | body { 21 | font-size: 18px; 22 | } 23 | } 24 | 25 | h1, h2, h3, h4, h5, h6 { 26 | color: @header-color; 27 | } 28 | 29 | a[href] { 30 | color: @link-color; 31 | text-decoration: none; 32 | } 33 | 34 | a[id]:not([href]) { 35 | // This is quite a badass hack. 36 | // We really those anchors despite the navbar! 37 | position: relative; 38 | top: -@navbar-height; 39 | } 40 | 41 | pre, code { 42 | font-family: Consolas, 'Source Code Pro', Menlo, monospace; 43 | background: #F9F8F7; 44 | color: #484A4C; 45 | // font-size: 1em; 46 | // letter-spacing: -0.015em; 47 | } 48 | 49 | a code { 50 | color: inherit; 51 | } 52 | 53 | code { 54 | margin: -0.05rem -0.15em; 55 | padding: 0.05rem 0.35em; 56 | } 57 | 58 | blockquote { 59 | margin: 1rem 0; 60 | padding: 0 1rem; 61 | color: #727476; 62 | border-left: solid 3px #DCDAD9; 63 | } 64 | 65 | blockquote > :first-child { 66 | margin-top: 0; 67 | } 68 | 69 | blockquote > :last-child { 70 | margin-bottom: 0; 71 | } 72 | 73 | hr { 74 | border: 1px solid; 75 | color: @body-color; 76 | opacity: 0.1; 77 | } 78 | 79 | 80 | // Markdown 81 | 82 | .codeBlock { 83 | -webkit-overflow-scrolling: touch; 84 | background: #FCFBFA; 85 | border-left: solid 3px #ECEAE9; 86 | box-sizing: border-box; 87 | display: block; 88 | // font-size: 0.875em; 89 | margin: 0.5rem 0; 90 | overflow-y: scroll; 91 | padding: 0.5rem 8px 0.5rem 12px; 92 | white-space: pre; 93 | } 94 | 95 | .t.blockParams { 96 | padding-left: 2ch; 97 | } 98 | 99 | // TODO: not random colors 100 | 101 | .token.punctuation, 102 | .token.ignore, 103 | .t.interfaceDef, 104 | .t.member, 105 | .t.callSig { 106 | color: #808890; 107 | } 108 | 109 | .token.function, 110 | .token.class-name, 111 | .token.qualifier, 112 | .t.fnQualifier, 113 | .t.fnName { 114 | color: #32308E; 115 | } 116 | 117 | .token.primitive, 118 | .t.primitive { 119 | color: #922; 120 | } 121 | 122 | .token.number, 123 | .t.typeParam { 124 | color: #905; 125 | } 126 | 127 | .t.typeQualifier, 128 | .t.typeName { 129 | color: #013679; 130 | } 131 | 132 | .t.param { 133 | color: #945277; 134 | } 135 | 136 | .t.memberName { 137 | color: teal; 138 | } 139 | 140 | .token.block-keyword, 141 | .token.keyword, 142 | .t.keyword { 143 | color: #A51; 144 | } 145 | 146 | .token.string, 147 | .token.regex { 148 | color: #df5050; 149 | } 150 | 151 | .token.operator { 152 | color: #a67f59; 153 | } 154 | 155 | .token.comment { 156 | color: #998; 157 | font-style: italic; 158 | } 159 | -------------------------------------------------------------------------------- /site/client.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import IndexPage from './IndexPage'; 4 | 5 | render( 6 | , 9 | document 10 | ); 11 | -------------------------------------------------------------------------------- /site/components/CodeBlock.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import { findDOMNode } from 'react-dom'; 3 | import ReactUpdates from 'react-dom/lib/ReactUpdates'; 4 | import StaticHTMLBlock from './StaticHTMLBlock'; 5 | 6 | import './CodeBlock.less'; 7 | 8 | let preferredSyntax = 'es5'; 9 | let observers = []; 10 | 11 | function subscribe(observer) { 12 | observers.push(observer); 13 | return () => { 14 | observers.slice(observers.indexOf(observer), 1); 15 | }; 16 | } 17 | 18 | function setPreferredSyntax(syntax) { 19 | preferredSyntax = syntax; 20 | observers.forEach(o => o(preferredSyntax)); 21 | } 22 | 23 | export default class CodeBlock extends Component { 24 | static propTypes = { 25 | es5: PropTypes.string, 26 | es6: PropTypes.string, 27 | es7: PropTypes.string 28 | }; 29 | 30 | static defaultProps = { 31 | es5: '', 32 | es6: '', 33 | es7: '' 34 | }; 35 | 36 | constructor(props) { 37 | super(props); 38 | this.state = { 39 | chosen: false, 40 | syntax: this.props.es5.trim().length && 'es5' || 41 | this.props.es6.trim().length && 'es6' || 42 | this.props.es7.trim().length && 'es7' 43 | }; 44 | } 45 | 46 | componentDidMount() { 47 | this.unsubscribe = subscribe(this.handlePreferredSyntaxChange.bind(this)); 48 | } 49 | 50 | handlePreferredSyntaxChange(syntax) { 51 | if (this.state.chosen || this.state.syntax === syntax) { 52 | return; 53 | } 54 | 55 | if (this.props[syntax].trim().length) { 56 | this.setState({ 57 | syntax 58 | }); 59 | } 60 | } 61 | 62 | componentWillUnmount() { 63 | this.unsubscribe(); 64 | } 65 | 66 | handleSyntaxClick(syntax) { 67 | this.setState({ 68 | syntax, 69 | chosen: true 70 | }); 71 | 72 | const scrollTopBefore = findDOMNode(this).getBoundingClientRect().top; 73 | setPreferredSyntax(syntax); 74 | ReactUpdates.flushBatchedUpdates(); 75 | const scrollTopAfter = findDOMNode(this).getBoundingClientRect().top; 76 | 77 | window.scroll( 78 | window.pageXOffset || window.scrollX, 79 | (window.pageYOffset || window.scrollY) - (scrollTopBefore - scrollTopAfter) 80 | ); 81 | } 82 | 83 | render() { 84 | return ( 85 |
86 |
    87 | {['es5', 'es6', 'es7'].map(this.renderSyntaxLink, this)} 88 |
89 |
90 | 91 |
92 |
93 | ); 94 | } 95 | 96 | renderSyntaxLink(syntax) { 97 | if (!this.props[syntax] || !this.props[syntax].trim().length) { 98 | return; 99 | } 100 | 101 | if (syntax === 'es5' && 102 | !this.props.es6.trim().length && 103 | !this.props.es7.trim().length) { 104 | return; 105 | } 106 | 107 | const active = this.state.syntax === syntax; 108 | return ( 109 |
  • 111 | 112 | {syntax.toUpperCase()} 113 | 114 |
  • 115 | ); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /site/components/CodeBlock.less: -------------------------------------------------------------------------------- 1 | @import '../constants.less'; 2 | 3 | .CodeBlock-tabs { 4 | padding: 0; 5 | } 6 | 7 | .CodeBlock-tab { 8 | display: inline-block; 9 | margin-right: 10px; 10 | } 11 | 12 | .CodeBlock-tab a { 13 | cursor: pointer; 14 | color: @body-color; 15 | } 16 | 17 | .CodeBlock-activeTab a { 18 | color: @accent-color; 19 | } -------------------------------------------------------------------------------- /site/components/Cover.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './Cover.less'; 3 | 4 | export default class Cover extends Component { 5 | render() { 6 | return ( 7 |
    8 |
    9 |

    10 | Drag and Drop for React 11 |

    12 |
    13 |
    14 | ); 15 | } 16 | } -------------------------------------------------------------------------------- /site/components/Cover.less: -------------------------------------------------------------------------------- 1 | @import '../constants.less'; 2 | 3 | .Cover { 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | // background-color: @color-darkBlue; 9 | padding: 2em; 10 | overflow: hidden; 11 | top: 0; 12 | width: 100%; 13 | z-index: 1; 14 | border-bottom: inset 1px solid rgba(255,255,255,0.1); 15 | 16 | .Cover-header { 17 | margin: 6rem 0 3rem; 18 | text-align: center; 19 | } 20 | 21 | .Cover-logo { 22 | margin: 0; 23 | font-size: 7vw; 24 | font-weight: normal; 25 | line-height: 1; 26 | letter-spacing: -0.025em; 27 | // color: @color-lightBlue; 28 | } 29 | 30 | .Cover-description { 31 | margin: 0; 32 | font-size: 2.25em; 33 | line-height: 1.3; 34 | // color: @color-lightBlue; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /site/components/Header.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import NavBar from './NavBar'; 3 | import Cover from './Cover'; 4 | import './Header.less'; 5 | 6 | export default class Header extends Component { 7 | render() { 8 | return ( 9 |
    10 | 11 |
    12 | ); 13 | } 14 | } -------------------------------------------------------------------------------- /site/components/Header.less: -------------------------------------------------------------------------------- 1 | .Header { 2 | overflow: hidden; //clearfix 3 | } 4 | -------------------------------------------------------------------------------- /site/components/NavBar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { DOCS_DEFAULT, EXAMPLES_DEFAULT } from '../Constants'; 3 | import './NavBar.less'; 4 | 5 | const GITHUB_URL = 'https://github.com/jchristman/react-context-menus'; 6 | const DOCS_LOCATION = DOCS_DEFAULT.location; 7 | const EXAMPLES_LOCATION = EXAMPLES_DEFAULT.location; 8 | 9 | export default class NavBar extends Component { 10 | render() { 11 | return ( 12 |
    13 |
    14 |
    15 | React Context Menus 16 |

    Context Menus for React

    17 |
    18 | 19 |
    20 | Docs 21 | Examples 22 | GitHub 23 |
    24 |
    25 |
    26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /site/components/NavBar.less: -------------------------------------------------------------------------------- 1 | @import '../constants.less'; 2 | 3 | .NavBar { 4 | z-index: 3; 5 | top: 0; 6 | position: fixed; 7 | width: 100%; 8 | padding: 0 @content-padding; 9 | background-color: fade(#fff, 90%); 10 | 11 | .NavBar-container { 12 | display: flex; 13 | justify-content: space-between; 14 | align-items: center; 15 | margin: 0 auto; 16 | height: @navbar-height; 17 | padding: @content-padding/2 0; 18 | border-bottom: 2px solid fade(@accent-color, 10%); 19 | } 20 | 21 | .NavBar-logo { 22 | color: @accent-color; 23 | line-height: 1.4; 24 | } 25 | 26 | .NavBar-logoTitle { 27 | font-weight: bold; 28 | font-size: 1.125em; 29 | } 30 | 31 | .NavBar-logoDescription { 32 | margin: 0; 33 | font-size: .875em; 34 | } 35 | 36 | .NavBar-link { 37 | color: @accent-color; 38 | } 39 | 40 | .NavBar-link + .NavBar-link { 41 | margin-left: 1em; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /site/components/PageBody.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './PageBody.less'; 3 | 4 | export default class PageBody extends Component { 5 | static propTypes = { 6 | hasSidebar: React.PropTypes.bool 7 | }; 8 | 9 | render() { 10 | var {hasSidebar, html, ...props} = this.props; 11 | return ( 12 |
    13 |
    14 | {this.props.children} 15 |
    16 |
    17 | ); 18 | } 19 | } -------------------------------------------------------------------------------- /site/components/PageBody.less: -------------------------------------------------------------------------------- 1 | @import '../constants.less'; 2 | 3 | .PageBody { 4 | padding: @content-padding*4 @content-padding; 5 | background-color: #fff; 6 | 7 | .PageBody-container { 8 | display: flex; 9 | flex-direction: column; 10 | margin: 0 auto; 11 | max-width: @content-width; 12 | 13 | @media only screen and (min-width: @screen-tablet) { 14 | flex-direction: row; 15 | } 16 | } 17 | } 18 | 19 | .PageBody.PageBody--hasSidebar { 20 | .PageBody-container { 21 | @media only screen and (min-width: @screen-tablet) { 22 | margin-left: @sidebar-width + @content-padding*2; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /site/components/SideBar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './SideBar.less'; 3 | 4 | export default class SideBar extends Component { 5 | render() { 6 | return ( 7 |
    8 |
    9 | {this.props.groups.map(this.renderGroup, this)} 10 |
    11 |
    12 | ); 13 | } 14 | 15 | renderGroup({ title, pages}, index) { 16 | return ( 17 |
    18 |

    19 | {title} 20 |

    21 | {Object.keys(pages).map(key => this.renderLink(pages[key], key))} 22 |
    23 | ); 24 | } 25 | 26 | renderLink({ title, location }, key) { 27 | const arrow = ; 28 | 29 | let linkClass = 'SideBar-item'; 30 | if (this.props.example.location === location) { 31 | linkClass += ' SideBar-item--selected'; 32 | } 33 | 34 | return ( 35 | 36 | {title} 37 | {arrow} 38 | 39 | ); 40 | } 41 | } -------------------------------------------------------------------------------- /site/components/SideBar.less: -------------------------------------------------------------------------------- 1 | @import '../constants.less'; 2 | 3 | .SideBar { 4 | flex-shrink: 0; 5 | 6 | @media only screen and (min-width: @screen-tablet) { 7 | position: fixed; 8 | left: @content-padding; 9 | top: @navbar-height; 10 | bottom: 0; 11 | width: @sidebar-width; 12 | overflow: scroll; 13 | padding: @content-padding*2 0; 14 | } 15 | 16 | .SideBar-group + .SideBar-group { 17 | margin-top: @content-padding; 18 | } 19 | 20 | .SideBar-groupTitle { 21 | border-bottom: 2px solid fade(@accent-color, 10%); 22 | padding-bottom: .5em; 23 | margin: 0 0 .5em 0; 24 | } 25 | 26 | .SideBar-item { 27 | display: block; 28 | color: @body-color; 29 | 30 | &.SideBar-item--selected { 31 | color: @accent-color; 32 | position: relative; 33 | 34 | &:after { 35 | content: '·'; 36 | position: absolute; 37 | left: -.5em; top: 0; bottom: 0; 38 | // color: fade(@accent-color, 10%); 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /site/components/StaticHTMLBlock.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import CodeBlock from './CodeBlock'; 3 | 4 | export default class StaticHTMLBlock extends Component { 5 | static propTypes = { 6 | html: React.PropTypes.string.isRequired 7 | }; 8 | 9 | render() { 10 | const { html } = this.props; 11 | 12 | // Here goes a really hack-ish way to convert 13 | // areas separated by Markdown
    s into code tabs. 14 | 15 | const blocks = html.split('
    '); 16 | const elements = []; 17 | 18 | let es5Content = null; 19 | let es6Content = null; 20 | let es7Content = null; 21 | 22 | for (let i = 0; i < blocks.length; i++) { 23 | const content = blocks[i]; 24 | 25 | switch (i % 4) { 26 | case 0: 27 | elements.push( 28 |
    31 | ); 32 | break; 33 | case 1: 34 | es5Content = content; 35 | break; 36 | case 2: 37 | es6Content = content; 38 | break; 39 | case 3: 40 | es7Content = content; 41 | elements.push( 42 | 46 | ); 47 | break; 48 | } 49 | } 50 | 51 | return ( 52 |
    53 | {elements} 54 |
    55 | ); 56 | } 57 | } -------------------------------------------------------------------------------- /site/constants.less: -------------------------------------------------------------------------------- 1 | @accent-color: #0074D9; 2 | 3 | @link-color: @accent-color; 4 | @header-color: #212325; 5 | @body-color: #626466; 6 | 7 | @navbar-height: 5em; 8 | 9 | @content-width: 52em; 10 | @content-padding: 1.5em; 11 | 12 | @sidebar-width: 12em; 13 | 14 | @screen-tablet: 680px; 15 | -------------------------------------------------------------------------------- /site/pages/APIPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Header from '../components/Header'; 3 | import PageBody from '../components/PageBody'; 4 | import SideBar from '../components/SideBar'; 5 | import StaticHTMLBlock from '../components/StaticHTMLBlock'; 6 | import { APIPages } from '../Constants'; 7 | 8 | export default class APIPage extends Component { 9 | render() { 10 | return ( 11 |
    12 |
    13 | 14 | 15 | 19 | 20 | 21 | 22 |
    23 | ); 24 | } 25 | } -------------------------------------------------------------------------------- /site/pages/ExamplePage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Header from '../components/Header'; 3 | import PageBody from '../components/PageBody'; 4 | import SideBar from '../components/SideBar'; 5 | import { ExamplePages } from '../Constants'; 6 | 7 | export default class ExamplesPage extends Component { 8 | render() { 9 | return ( 10 |
    11 |
    12 | 13 | 17 | 18 | {this.props.children} 19 | 20 |
    21 | ); 22 | } 23 | } -------------------------------------------------------------------------------- /site/pages/HomePage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Header from '../components/Header'; 3 | import PageBody from '../components/PageBody'; 4 | import StaticHTMLBlock from '../components/StaticHTMLBlock'; 5 | import IndexHTML from '../../docs/index.md'; 6 | 7 | export default class HomePage extends Component { 8 | render() { 9 | return ( 10 |
    11 |
    12 | 13 | 14 | 15 | 16 |
    17 | ); 18 | } 19 | } -------------------------------------------------------------------------------- /site/renderPath.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import IndexPage from './IndexPage'; 3 | 4 | export default function renderPath(path, props, onRender) { 5 | onRender( 6 | IndexPage.renderToString(props) 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /site/webpack-client.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | //var resolvers = require('../scripts/resolvers'); 4 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 5 | 6 | var isDev = process.env.NODE_ENV !== 'production'; 7 | 8 | console.log(process.cwd() + '/__site__/'); 9 | module.exports = { 10 | 11 | devtool: isDev ? 'cheap-eval-source-map' : 'source-map', 12 | 13 | entry: [ 14 | path.join(__dirname, 'client.js') 15 | ].concat(isDev ? [ 16 | 'webpack-dev-server/client?http://localhost:8080', 17 | 'webpack/hot/only-dev-server' 18 | ] : []), 19 | output: { 20 | path: process.cwd() + '/__site__/', 21 | filename: isDev ? '[name].js' : '[name]-[hash].js', 22 | publicPath: '' 23 | }, 24 | 25 | target: 'web', 26 | 27 | module: { 28 | loaders: [ 29 | { 30 | test: /\.md$/, 31 | loader: [ 32 | 'html-loader?{"minimize":false}', 33 | path.join(__dirname, '../scripts/markdownLoader') 34 | ].join('!') 35 | }, 36 | { 37 | test: /\.js$/, 38 | exclude: /node_modules/, 39 | loaders: isDev ? ['react-hot-loader', 'babel-loader'] : ['babel-loader'] 40 | }, 41 | { 42 | test: /\.less$/, 43 | loader: ExtractTextPlugin.extract( 44 | { 45 | fallback: 'style-loader', 46 | use: ['css-loader','less-loader'] 47 | } 48 | ) 49 | }, 50 | { 51 | test: /\.png$/, 52 | loader: 'file-loader', 53 | query: { mimetype: 'image/png', name: 'images/[name]-[hash].[ext]' } 54 | } 55 | ] 56 | }, 57 | 58 | resolve: { 59 | alias: { 60 | 'react-context-menus/modules': path.join(__dirname, '../src'), 61 | 'react-context-menus': path.join(__dirname, '../src') 62 | } 63 | }, 64 | 65 | plugins: [ 66 | new ExtractTextPlugin( 67 | isDev ? '[name].css' : '[name]-[hash].css' 68 | ), 69 | new webpack.optimize.OccurrenceOrderPlugin(), 70 | new webpack.DefinePlugin({ 71 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 72 | '__DEV__': JSON.stringify(isDev || true) 73 | }), 74 | //resolvers.resolveHasteDefines, 75 | ] 76 | }; 77 | 78 | if (process.env.NODE_ENV === 'production') { 79 | module.exports.plugins.push( 80 | new webpack.optimize.UglifyJsPlugin({ 81 | compressor: { 82 | warnings: false 83 | } 84 | }) 85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /site/webpack-prerender.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | //var resolvers = require('../scripts/resolvers'); 4 | 5 | var isDev = process.env.NODE_ENV !== 'production'; 6 | console.log(process.cwd() + '/__site_prerender__/'); 7 | module.exports = { 8 | entry: path.join(__dirname, 'renderPath.js'), 9 | 10 | output: { 11 | path: process.cwd() + '/__site_prerender__/', 12 | filename: 'renderPath.js', 13 | libraryTarget: 'commonjs2', 14 | }, 15 | 16 | target: 'node', 17 | 18 | module: { 19 | loaders: [ 20 | { 21 | test: /\.md$/, 22 | loader: [ 23 | 'html-loader?{"minimize":false}', 24 | path.join(__dirname, '../scripts/markdownLoader') 25 | ].join('!') 26 | }, 27 | { 28 | test: /\.js$/, 29 | loader: 'babel-loader', 30 | exclude: /node_modules/ 31 | }, 32 | { 33 | test: /\.css$/, 34 | loader: 'null-loader' 35 | }, 36 | { 37 | test: /\.less$/, 38 | loader: 'null-loader' 39 | }, 40 | { 41 | test: /\.png$/, 42 | loader: 'file-loader', 43 | query: { mimetype: 'image/png', name: 'images/[name]-[hash].[ext]' } 44 | } 45 | ] 46 | }, 47 | 48 | resolve: { 49 | alias: { 50 | 'react-context-menus/modules': path.join(__dirname, '../src'), 51 | 'react-context-menus': path.join(__dirname, '../src') 52 | } 53 | }, 54 | 55 | plugins: [ 56 | new webpack.optimize.OccurrenceOrderPlugin(), 57 | new webpack.DefinePlugin({ 58 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 59 | '__DEV__': JSON.stringify(isDev || true) 60 | }), 61 | //resolvers.resolveHasteDefines, 62 | ] 63 | }; 64 | 65 | if (process.env.NODE_ENV === 'production') { 66 | module.exports.plugins.push( 67 | new webpack.optimize.UglifyJsPlugin({ 68 | compressor: { 69 | warnings: false 70 | } 71 | }) 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /src/context_menu.js: -------------------------------------------------------------------------------- 1 | import React, {cloneElement} from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import invariant from 'invariant'; 4 | import _ from 'underscore'; 5 | import Menu from 'react-menus2'; 6 | 7 | console.log(Menu); 8 | 9 | const ContextMenu = (menu_items, _options = {}) => { 10 | return (ChildComponent) => { 11 | const Container = class extends React.Component { 12 | constructor(props, context) { 13 | super(props, context); 14 | 15 | this.options = typeof _options === 'function' ? (_props) => _options(_props) : (_props) => _options; 16 | const options = this.options(props); 17 | 18 | this.state = { 19 | showContextMenu: options.show !== undefined && options.show, 20 | x: options.at !== undefined && options.at.x, 21 | y: options.at !== undefined && options.at.y 22 | }; 23 | 24 | // needed to add and remove event listeners.... 25 | this.last_clicked_element = undefined; 26 | this.clickable_elements = []; 27 | this.force_hide = this.force_hide.bind(this); 28 | this.hide = this.hide.bind(this); 29 | this.show = this.show.bind(this); 30 | } 31 | 32 | componentDidMount() { 33 | const options = this.options(this.props); 34 | 35 | this.child = ReactDOM.findDOMNode(this); 36 | this.container = document.createElement('div'); 37 | this.container.style.position = 'fixed'; 38 | this.container.style.top = 0; 39 | this.container.style.left = 0; 40 | this.container.style.width = 0; 41 | this.container.style.height = 0; 42 | this.updateContainer(options); 43 | this.child.appendChild(this.container); 44 | 45 | // Note that we are not using .bind(this), because we need to remove 46 | // the listener later. The bind happens in the constructor 47 | document.addEventListener('click', this.force_hide, false); 48 | document.addEventListener('contextmenu', this.hide, false); 49 | this.rebind(); 50 | 51 | this._renderLayer(); 52 | 53 | this._mounted = true; 54 | } 55 | 56 | clean() { 57 | this.clickable_elements = _.filter(this.clickable_elements, (element) => element !== null); 58 | } 59 | 60 | rebind() { 61 | _.each(this.clickable_elements, (element) => element.addEventListener('contextmenu', this.show, false)); 62 | } 63 | 64 | cleanAndRebind() { 65 | this.clean(); 66 | this.rebind(); 67 | } 68 | 69 | updateContainer(options) { 70 | if (options.container !== undefined && this.container !== undefined) { 71 | if (options.container.zIndex !== undefined) this.container.style.zIndex = options.container.zIndex; 72 | } 73 | } 74 | 75 | componentWillReceiveProps(nextProps) { 76 | const options = this.options(nextProps); 77 | if (options.show !== undefined) this._mounted && this.setState({ showContextMenu: options.show }) 78 | if (options.at !== undefined) this._mounted && this.setState({ ...options.at }) 79 | this.updateContainer(options); 80 | } 81 | 82 | componentDidUpdate() { 83 | this._renderLayer(); 84 | } 85 | 86 | componentWillUnmount() { 87 | if (this.container !== null) 88 | ReactDOM.unmountComponentAtNode(this.container); 89 | 90 | // Note that we are not using .bind(this), because we need to remove 91 | // the listener later. The bind happens in the constructor 92 | document.removeEventListener('click', this.force_hide, false); 93 | document.removeEventListener('contextmenu', this.hide, false); 94 | _.each(this.clickable_elements, (element) => { 95 | if (element !== null) element.removeEventListener('contextmenu', this.show, false) 96 | }); 97 | 98 | this._mounted = false; 99 | } 100 | 101 | _renderLayer() { 102 | if (this.state.showContextMenu) { 103 | // If the menu_items var is a function, let's call it with the props. 104 | const menu = typeof menu_items === 'function' ? menu_items(this.props) : menu_items; 105 | 106 | // Then correct the items to fix their onClick methods to be useful and have the props of the clicked element 107 | let wrapped_menu_items = menu.map((item) => { 108 | // Copy the item 109 | let new_item = typeof item === 'object' ? _.extend({}, item) : item; 110 | if (new_item.onClick !== undefined) new_item.onClick = (event, item_props, index) => { item.onClick(event, this.props, index) }; 111 | return new_item; 112 | }); 113 | 114 | const options = this.options(this.props); 115 | const theme = options.theme || {}; 116 | const style = options.style || {}; 117 | // Finally, render it to the container 118 | try { 119 | ReactDOM.render(, 125 | this.container); 126 | } catch (e) { 127 | 128 | } 129 | } else { 130 | ReactDOM.unmountComponentAtNode(this.container); 131 | } 132 | } 133 | 134 | 135 | render() { 136 | return ( 137 | 141 | ); 142 | } 143 | 144 | cloneWithRef(element, newRef) { 145 | const previousRef = element.ref; 146 | invariant(typeof previousRef !== 'string', 147 | 'Cannot connect ContextMenu to an element with an existing string ref.'); 148 | 149 | if (!previousRef) { 150 | return cloneElement(element, { ref: newRef }); 151 | } 152 | 153 | return cloneElement(element, { 154 | ref: (node) => { 155 | newRef(node); 156 | previousRef && previousRef(node); 157 | } 158 | }); 159 | } 160 | 161 | // ----- Context Menu Methods ----- // 162 | // This will almost certainly happen before we mount (the first time). 163 | // but since this will likely happen a lot if things are rerendering and 164 | // the component is not changing its mounted state, we call clean and rebind 165 | // from here. 166 | connectContextMenu(react_element) { 167 | this.clickable_react_element = react_element; 168 | this.clickable_react_element = this.cloneWithRef(this.clickable_react_element, (node) => this.clickable_elements.push(node)); 169 | this.cleanAndRebind(); 170 | return this.clickable_react_element; 171 | } 172 | 173 | show(event) { 174 | event.preventDefault(); 175 | 176 | this.last_clicked_element = event.target; 177 | 178 | let bounds = this.child.getBoundingClientRect(); 179 | let x = event.clientX; 180 | let y = event.clientY; 181 | 182 | const state = { x, y, showContextMenu: true }; 183 | this._mounted && this.setState(state); 184 | } 185 | 186 | force_hide(event) { 187 | let button = event.which || event.button; 188 | if (button === 1) 189 | setTimeout(() => this.hide(event, true), 0); // We do this to allow the click to register if it hasn't yet 190 | } 191 | 192 | hide(event, force) { 193 | if (event.target !== this.last_clicked_element || force) { 194 | const state = { showContextMenu: false }; 195 | this._mounted && this.setState(state); 196 | } 197 | } 198 | } 199 | 200 | return Container; 201 | } 202 | } 203 | 204 | export default ContextMenu; 205 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import ContextMenu from './context_menu.js'; 2 | export default ContextMenu; 3 | --------------------------------------------------------------------------------