├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── .yarnclean ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── DynamicTabBadge.js ├── DynamicTabContent.js ├── app.js ├── icomoon │ ├── Read Me.txt │ ├── fonts │ │ ├── icomoon.eot │ │ ├── icomoon.svg │ │ ├── icomoon.ttf │ │ └── icomoon.woff │ ├── selection.json │ └── style.css ├── icon.png └── index.html ├── karma.conf.js ├── lib ├── components │ ├── CloseIcon.js │ ├── CustomDraggable.js │ ├── Tab.js │ ├── TabContainer.js │ ├── TabStyles.js │ └── Tabs.js ├── helpers │ ├── styleOverride.js │ └── utils.js └── index.js ├── package.json ├── src ├── components │ ├── CloseIcon.js │ ├── CustomDraggable.js │ ├── Tab.js │ ├── TabContainer.js │ ├── TabStyles.js │ └── Tabs.js ├── helpers │ ├── styleOverride.js │ └── utils.js └── index.js ├── test ├── components │ ├── CloseIcon_spec.js │ ├── TabContainer_spec.js │ ├── Tab_spec.js │ └── Tabs_spec.js ├── helpers │ ├── styleOverride_spec.js │ └── utils_spec.js └── triggerEvent.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "react", 5 | "stage-0" 6 | ], 7 | "env": { 8 | "production": { 9 | "plugins": ["minify-dead-code-elimination"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | [*.js] 12 | charset = utf-8 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [{package.json}] 17 | indent_style = space 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | example/** 2 | node_modules/** 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb/base", 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "es6": true, 7 | "mocha": true 8 | }, 9 | "ecmaFeatures": { 10 | "jsx": true 11 | }, 12 | "parser": "babel-eslint", 13 | "plugins": [ 14 | "react" 15 | ], 16 | "rules": { 17 | "max-len": [2, 160, 2, { 18 | "ignoreUrls": true, 19 | "ignoreComments": false 20 | }], 21 | "quotes": [1, "single", "avoid-escape"], 22 | "react/display-name": 0, 23 | "react/jsx-no-undef": 1, 24 | "react/jsx-uses-react": 1, 25 | "react/no-did-mount-set-state": 1, 26 | "react/no-did-update-set-state": 1, 27 | "react/no-multi-comp": 1, 28 | "react/prop-types": [1, { "ignore": ["children", "className"] }], 29 | "react/react-in-jsx-scope": 1, 30 | "react/self-closing-comp": 1, 31 | "react/jsx-uses-vars": 1 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | dist 5 | example/bundle.js 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | services: 5 | - xvfb 6 | -------------------------------------------------------------------------------- /.yarnclean: -------------------------------------------------------------------------------- 1 | # test directories 2 | __tests__ 3 | test 4 | tests 5 | powered-test 6 | 7 | # asset directories 8 | docs 9 | doc 10 | website 11 | images 12 | assets 13 | 14 | # examples 15 | example 16 | examples 17 | 18 | # code coverage directories 19 | coverage 20 | .nyc_output 21 | 22 | # build scripts 23 | Makefile 24 | Gulpfile.js 25 | Gruntfile.js 26 | 27 | # configs 28 | appveyor.yml 29 | circle.yml 30 | codeship-services.yml 31 | codeship-steps.yml 32 | wercker.yml 33 | .tern-project 34 | .gitattributes 35 | .editorconfig 36 | .*ignore 37 | .eslintrc 38 | .jshintrc 39 | .flowconfig 40 | .documentup.json 41 | .yarn-metadata.json 42 | .travis.yml 43 | 44 | # misc 45 | *.md 46 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Change Log 2 | 3 | ### Ver 0.10.1 4 | 5 | * #101 [Received `false` for a non-boolean attribute `uncloseable`](https://github.com/georgeOsdDev/react-draggable-tab/issues/101) 6 | ### Ver 0.10.0 7 | 8 | * #99 [Fix this.unbindShortcuts is not a function](https://github.com/georgeOsdDev/react-draggable-tab/issues/99) 9 | * Fix falling unit tests 10 | 11 | ### Ver 0.9.0 12 | 13 | This version contains a breaking change. The name of the Tab 'disableClose' property, 14 | which shadowed a standard property name and resulted in a React warning, has been changed 15 | to 'unclosable'. 16 | 17 | **TESTS ARE BROKEN**. `react-addons-test-utils` has a hard dependency on React 15. For 18 | React 16, Facebook has moved the utilities into `react-dom/test-utils` and 19 | `react-test-renderer/shallow`, and they are quite a bit different. 20 | 21 | * #73 [Invariant Violation: addComponentAsRefTo(...): ...](https://github.com/georgeOsdDev/react-draggable-tab/issues/73) 22 | may be better. The React version has been updated, but react-draggable-tab is still using string refs, which is deprecated. 23 | * #90 [React.PropTypes should be replaced by just PropTypes](https://github.com/georgeOsdDev/react-draggable-tab/issues/90) 24 | * #92 [Update to React v16](https://github.com/georgeOsdDev/react-draggable-tab/issues/92) 25 | Updated all libraries. Uses React 16.2.0. 26 | * #93 [Resolve all eslint "airbnb" errors](https://github.com/georgeOsdDev/react-draggable-tab/issues/93) 27 | (except in tests) 28 | 29 | ### Ver 0.8.1 30 | 31 | * #80 [Fix tabAddButton position](https://github.com/georgeOsdDev/react-draggable-tab/issues/80) 32 | Thanks @warent 33 | * #81 [Added prop to disable the dragging of tabs]()https://github.com/georgeOsdDev/react-draggable-tab/issues/81 34 | Thanks @erangil2 35 | 36 | ### Ver 0.8.0 37 | 38 | * #77 [Add shouldTabClose](https://github.com/georgeOsdDev/react-draggable-tab/issues/77) 39 | 40 | ### Ver 0.7.0 41 | 42 | * #67 [Fix TypeError when using SVGs as title](https://github.com/georgeOsdDev/react-draggable-tab/issues/67) 43 | Thanks @HaNdTriX 44 | * #68 [Improve tab rendering perfomance](https://github.com/georgeOsdDev/react-draggable-tab/issues/68) 45 | 46 | ### Ver 0.6.1 47 | 48 | * #66 [Removes console warning in React15](https://github.com/georgeOsdDev/react-draggable-tab/issues/66) 49 | Thanks @uriklar 50 | 51 | ### Ver 0.6.0 52 | 53 | * #63 [React v15 support](https://github.com/georgeOsdDev/react-draggable-tab/issues/63) 54 | 55 | ### Ver 0.5.1 56 | * #59 [Allow overriding TabStyles.wrapper using props](https://github.com/georgeOsdDev/react-draggable-tab/issues/59) 57 | 58 | ### Ver 0.5.0 59 | * #53 [Context menu on right click](https://github.com/georgeOsdDev/react-draggable-tab/issues/53) 60 | `onTabDoubleClick`, `onTabMouseEnter` and `onTabMouseEnter` are removed from `Tabs`. 61 | All `onXXX` handler except `onClick` are now used on `Tab`. 62 | * Update dependencies libraries version [cf65dbc](https://github.com/georgeOsdDev/react-draggable-tab/commit/cf65dbc8f756561536f53f5e3960bf86afebdc73) 63 | 64 | ### Ver 0.4.3 65 | 66 | * #48 [When tab title is truncated onhover does not show whole title](https://github.com/georgeOsdDev/react-draggable-tab/issues/48) 67 | * #50 [Unable to add styles or css classes to the tab content div](https://github.com/georgeOsdDev/react-draggable-tab/issues/50) 68 | 69 | ### Ver 0.4.2 70 | 71 | * #45 [Unexpected diff between src and lib](https://github.com/georgeOsdDev/react-draggable-tab/issues/45) 72 | 73 | ### Ver 0.4.1 74 | 75 | * #33 [Add hover effect on mouseOver like chrome](https://github.com/georgeOsdDev/react-draggable-tab/issues/33) 76 | * #38 [Publish transpiled code](https://github.com/georgeOsdDev/react-draggable-tab/issues/38) 77 | * #40 [Tab start moving from strange position](https://github.com/georgeOsdDev/react-draggable-tab/issues/33) 78 | 79 | ### Ver 0.4.0 80 | 81 | This version updated react from ^0.13.3 to 0.14.X, react-draggable from ^0.8.0 to 1.1.X, and also other dependencies. 82 | Several changes have been made for adopting react v0.14.x. 83 | 84 | * #35 [Update dependencies](https://github.com/georgeOsdDev/react-draggable-tab/issues/35) 85 | * #32 [Add `keepSelectedTab` option](https://github.com/georgeOsdDev/react-draggable-tab/issues/32) 86 | 87 | ### Ver 0.3.3 88 | 89 | * #30 [Switch tab position while dragging](https://github.com/georgeOsdDev/react-draggable-tab/issues/30) 90 | 91 | ### Ver 0.3.2 92 | 93 | * #27 [Support keyboard shortcut](https://github.com/georgeOsdDev/react-draggable-tab/issues/27) 94 | 95 | ### Ver 0.3.1 96 | 97 | * #26 [Enable to use element for tab title](https://github.com/georgeOsdDev/react-draggable-tab/issues/26) 98 | 99 | ### Ver 0.3.0 100 | 101 | This release contains breaking change. 102 | 103 | * #20 [Remove 'ed' from event handler name](https://github.com/georgeOsdDev/react-draggable-tab/issues/20) (Breaking change) 104 | * #21 [Unexpected behavior with NODE_ENV=production](https://github.com/georgeOsdDev/react-draggable-tab/issues/21) 105 | 106 | ### Ver 0.2.3 107 | 108 | * #16 [Add doubleClick handler on tab title](https://github.com/georgeOsdDev/react-draggable-tab/issues/16) 109 | 110 | ### Ver 0.2.2 111 | 112 | * #13 [Improve tabBarAfter style](https://github.com/georgeOsdDev/react-draggable-tab/issues/13) 113 | 114 | ### Ver 0.2.1 115 | 116 | * #5 [Enable to add badge / favicon like chrome](https://github.com/georgeOsdDev/react-draggable-tab/issues/5) 117 | * #6 [Add style on parent div](https://github.com/georgeOsdDev/react-draggable-tab/issues/6) 118 | * #8 [Show closed tab when it supplied again with same key](https://github.com/georgeOsdDev/react-draggable-tab/issues/8) 119 | * #9 [Keep all tab content inside Tabs](https://github.com/georgeOsdDev/react-draggable-tab/issues/9) 120 | 121 | ### Ver 0.2.0 122 | 123 | * #1 [Improve tab title color](https://github.com/georgeOsdDev/react-draggable-tab/issues/1) 124 | * #2 [onTabSelected, onTabClosed, onTabPositionChanged should be called in setState callback](https://github.com/georgeOsdDev/react-draggable-tab/issues/2) 125 | * #3 [Enable to set style and class on each Tab](https://github.com/georgeOsdDev/react-draggable-tab/issues/3) 126 | 127 | ### Ver 0.1.0 Initial release 128 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2015 - 2016 Takeharu.Oshida 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React-draggable-tab [![Build Status](https://travis-ci.org/georgeOsdDev/react-draggable-tab.svg?branch=develop)](https://travis-ci.org/georgeOsdDev/react-draggable-tab) [![npm version](https://badge.fury.io/js/react-draggable-tab.svg)](http://badge.fury.io/js/react-draggable-tab) 2 | 3 | 4 | [![Gyazo](http://i.gyazo.com/42d408d288292f62fbb8d650897acbc4.gif)](http://gyazo.com/42d408d288292f62fbb8d650897acbc4) 5 | 6 | Atom like draggable tab react component. 7 | 8 | ## Demo 9 | 10 | [View Demo](http://georgeosddev.github.io/react-draggable-tab/example/) 11 | 12 | ## Installation 13 | 14 | ```bash 15 | npm install --save react-draggable-tab 16 | ``` 17 | React v0.14 is supported from react-draggable-tab v0.4.0. 18 | For React v0.13.x, please use react-draggable-tab v0.3.3. 19 | 20 | ## API 21 | 22 | ### `Tab` 23 | 24 | `Tab` is just a case class to check props. 25 | `props.children` will rendered into content area. 26 | 27 | #### Props 28 | 29 | * `key`: *unique* key in `TabList`. 30 | `PropTypes.string.isRequired` 31 | 32 | * `beforeTitle`: element to show in tab. eg icon. 33 | `PropTypes.element` 34 | 35 | * `title`: string or element to show in tab. 36 | `PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired` 37 | 38 | * `afterTitle`: element to show in tab. eg: notification badge 39 | `PropTypes.element` 40 | 41 | * `uncloseable`: If `true`, closeButton will not be appeared in tab. 42 | `PropTypes.bool` (Default `false`) 43 | 44 | ###### Style (for each tab) 45 | 46 | * `tabClassNames`: classNames which will be **added** to rendered elements. 47 | * `tab`: base `li` element of tab (defult: `rdTab`) 48 | * `tabBefore`: before element of `li` which emulate `:Before` selector (defult: `rdTabBefore`) 49 | * `tabAfter`: after element of `li` which emulate `:After` selector (defult: `rdTabAfter`) 50 | * `tabTitle`: `span` element of tab title (defult: `rdTabTitle`) 51 | * `tabBeforeTitle`: `span` element of tab before title (defult: `tabBeforeTitle`) 52 | * `tabAfterTitle`: `span` element of tab after title (defult: `tabAfterTitle`) 53 | * `tabCloseIcon`: base `span` element of close icon (defult: `rdCloseIcon`) 54 | * `tabActive`: selected tab's `li`, before, after (defult: `rdTabActive`) 55 | * `tabHover`: selected tab's `li`, before, after (defult: `rdTabHover`) 56 | 57 | * `tabStyles`: Inline styles which will be **overwritten** default and common-tabs inline styles. 58 | * `tab`: base `li` element of tab 59 | * `tabBefore`: before element of `li` which emulate `:Before` selector. 60 | * `tabAfter`: after element of `li` which emulate `:After` selector. 61 | * `tabTitle`: `span` element of tab title 62 | * `tabActive`: selected tab's `li` 63 | * `tabBeforeActive`: selected tab's `li` before 64 | * `tabAfterActive`: selected tab's `li` after 65 | * `tabTitleActive`: selected tab's title 66 | * `tabOnHover`: hovered tab's `li` 67 | * `tabBeforeOnHover`: hovered tab's `li` before 68 | * `tabAfterOnHover`: hovered tab's `li` after 69 | * `tabTitleOnHover`: hovered tab's title 70 | * `tabCloseIcon`: base `span` element of close icon 71 | * `tabCloseIconOnHover`: base `span` element of close icon when hover 72 | 73 | * `containerStyle`: style object which will be apply to container of rendered tab. 74 | 75 | * `hiddenContainerStyle`: style object which will be apply to container of hidden tab. 76 | 77 | 78 | ##### Events 79 | 80 | All other props like `onXX` handler set to `Tab` will be passed to rendered element except `onClick` 81 | You can use any `onXX` for [Supported events](https://facebook.github.io/react/docs/events.html#supported-events) for tab element. 82 | 83 | ### `Tabs` 84 | 85 | `Tabs` is container for tab. it will render tabBar and content of selected element. 86 | 87 | #### Props 88 | 89 | * `tabs`: Array of `Tab` elements. 90 | `PropTypes.arrayOf(PropTypes.element)` 91 | 92 | * `selectedTab`: key for selectedTab. 93 | `PropTypes.string` default to first tab. 94 | 95 | * `disableDrag`: disables the ability to drag the tabs. 96 | `React.PropTypes.bool` default is false. 97 | 98 | * `tabAddButton`: element for add button. 99 | `PropTypes.element` 100 | 101 | * `keepSelectedTab`: Prevent tab select on drag/move behind tab. 102 | `PropTypes.bool` default `false`. 103 | 104 | * `unclosable`: Disable tab to close. 105 | `PropTypes.bool` default `false`. 106 | 107 | * `shouldTabClose(e, key)`: will be called before tab close event, return false if you want to stop tab close process, default `true`; 108 | 109 | ###### Shortcut key binding 110 | * `shortCutKeys`: Short cut key bindings as [Mousetrap](https://craig.is/killing/mice) style. 111 | * `close`: key binding to close current tab (`onTabClose` will be called) 112 | * `create`: key binding to create tab (`onTabAddButtonClick` will be called) 113 | * `moveRight`: key binding to move right (`onTabSelect` will be called) 114 | * `moveLeft`: key binding to move left (`onTabSelect` will be called) 115 | 116 | ###### Style (All tabs will be apply these styles) 117 | 118 | * `tabsClassNames`: classNames which will be **added** to rendered elements. 119 | * `tabWrapper`: root wrapper `div` element (defult: `rdTabWrapper`) 120 | * `tabBar`: base `ul` element of tab bar (defult: `rdTabBar`) 121 | * `tabBarAfter`: after `span` element of tab bar which emulate `:After` selector (defult: `rdTabBarAfter`) 122 | * `tab`: base `li` element of tab (defult: `rdTab`) 123 | * `tabBefore`: before element of `li` which emulate `:Before` selector (defult: `rdTabBefore`) 124 | * `tabAfter`: after element of `li` which emulate `:After` selector (defult: `rdTabAfter`) 125 | * `tabTitle`: `span` element of tab title (defult: `rdTabTitle`) 126 | * `tabBeforeTitle`: `span` element of tab before title (defult: `rdTabBeforeTitle`) 127 | * `tabBeforeTitle`: `span` element of tab after title (defult: `rdTabAfterTitle`) 128 | * `tabCloseIcon`: base `span` element of close icon (defult: `rdCloseIcon`) 129 | * `tabActive`: selected tab's `li`, before, after (defult: `rdTabActive`) 130 | 131 | * `tabsStyles`: Inline styles which will be **overwritten** default inline styles. 132 | * `tabWrapper`: root wrapper `div` element 133 | * `tabBar`: base `ul` element of tab bar 134 | * `tabBarAfter`: after `span` element of tab bar which emulate `:After` selector 135 | * `tab`: base `li` element of tab 136 | * `tabBefore`: before element of `li` which emulate `:Before` selector. 137 | * `tabAfter`: after element of `li` which emulate `:After` selector. 138 | * `tabTitle`: `span` element of tab title 139 | * `tabActive`: selected tab's `li` 140 | * `tabBeforeActive`: selected tab's `li` before 141 | * `tabAfterActive`: selected tab's `li` after 142 | * `tabTitleActive`: selected tab's title 143 | * `tabCloseIcon`: base `span` element of close icon 144 | * `tabCloseIconOnHover`: base `span` element of close icon when hover 145 | 146 | ##### Events 147 | 148 | * `onTabSelect(e, key, currentTabs)`: Called when tab of key was selected. 149 | `currentTabs` is array of tabs elements sorted with current order. 150 | 151 | * `onTabClose(e, key, currentTabs)`: Called when tab of key was closed. 152 | `currentTabs` is array of tabs elements sorted with current order. 153 | 154 | * `onTabPositionChange(e, key, currentTabs)`: Called when tab of key was moved. 155 | `currentTabs` is array of tabs elements sorted with current order. 156 | 157 | * `onTabAddButtonClick(e, currentTabs)`: Called when `tab add button` was clicked. 158 | `currentTabs` is array of tabs elements sorted with current order. 159 | Basically you will concat `currentTabs` with new empty tab. 160 | 161 | ```javascript 162 | let newTabs = currentTabs.concat([newTab]); 163 | ``` 164 | 165 | ~~* `onTabDoubleClick(e, key)`: Called when `title` was double clicked.~~ Removed from v0.5.0 166 | 167 | ~~* `onTabMouseEnter(e, key)`: Called when mouse enter to `tab`.~~ Removed from v0.5.0 168 | 169 | ~~* `onTabMouseLeave(e, key)`: Called when mouse leave from `tab`.~~ Removed from v0.5.0 170 | 171 | ## Usage example 172 | 173 | ```javascript 174 | class App extends React.Component { 175 | constructor(props) { 176 | super(props); 177 | let icon = (); 178 | let fonticon = (); 179 | let badge = (); 180 | 181 | this.state = { 182 | tabs:[ 183 | ( 184 |
185 |

This tab cannot close

186 |
187 |
), 188 | ( 189 |
190 |

This is tab1

191 |
192 |
), 193 | ( 194 |
195 |
Lorem ipsum dolor sit amet, consectetur adipisicing elit,
196 |             
197 |
198 |
), 199 | ( 200 | 201 | ) 202 | ], 203 | badgeCount: 0 204 | }; 205 | } 206 | 207 | handleTabSelect(e, key, currentTabs) { 208 | console.log('handleTabSelect key:', key); 209 | this.setState({selectedTab: key, tabs: currentTabs}); 210 | } 211 | 212 | handleTabClose(e, key, currentTabs) { 213 | console.log('tabClosed key:', key); 214 | this.setState({tabs: currentTabs}); 215 | } 216 | 217 | handleTabPositionChange(e, key, currentTabs) { 218 | console.log('tabPositionChanged key:', key); 219 | this.setState({tabs: currentTabs}); 220 | } 221 | 222 | handleTabAddButtonClick(e, currentTabs) { 223 | // key must be unique 224 | const key = 'newTab_' + Date.now(); 225 | let newTab = ( 226 |
227 |

New Empty Tab

228 |
229 |
); 230 | let newTabs = currentTabs.concat([newTab]); 231 | 232 | this.setState({ 233 | tabs: newTabs, 234 | selectedTab: key 235 | }); 236 | } 237 | 238 | render() { 239 | 240 | return ( 241 | 259 | ) 260 | } 261 | }; 262 | ``` 263 | 264 | See also [example](https://github.com/georgeOsdDev/react-draggable-tab/tree/develop/example) 265 | 266 | 267 | ```bash 268 | npm install 269 | npm run start:example 270 | ``` 271 | 272 | ## Tests 273 | 274 | ```bash 275 | npm test 276 | ``` 277 | 278 | ## Contributors 279 | 280 | See [list](https://github.com/georgeOsdDev/react-draggable-tab/graphs/contributors) 281 | 282 | ## Known Issue 283 | 284 | * Dynamic tab content. 285 | 286 | `Tabs` do not care any change in `Tab` content. 287 | content needs update by your application side. 288 | See `3rdTab` in example. 289 | 290 | 291 | * `flex` style should be define in CSS for safari. 292 | See https://github.com/facebook/react/issues/2020 293 | 294 | In application, class `rdTabBar` or your custom class of `TabBar` needs `display: -webkit-flex` in CSS like below. 295 | ```css 296 | .myTabBar { 297 | display: -webkit-flex; 298 | } 299 | 300 | -------------------------------------------------------------------------------- /example/DynamicTabBadge.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import NotificationBadge, {Effect} from 'react-notification-badge'; 5 | 6 | class DynamicTabContent extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | badgeCount: 0 11 | }; 12 | } 13 | 14 | componentDidMount(){ 15 | this.startTimer(); 16 | } 17 | 18 | startTimer(){ 19 | setInterval(() => { 20 | this.setState({ 21 | badgeCount: this.state.badgeCount + 1 22 | }); 23 | }, 3000); 24 | } 25 | 26 | render() { 27 | return (); 28 | } 29 | } 30 | 31 | export default DynamicTabContent; 32 | -------------------------------------------------------------------------------- /example/DynamicTabContent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | 5 | class DynamicTabContent extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | textvalue: '' 10 | }; 11 | } 12 | 13 | _handleTextChange(e) { 14 | this.setState({textValue: e.target.value}); 15 | } 16 | 17 | render() { 18 | 19 | return ( 20 |
21 |

TAB3!!! This tab dynamically change

22 | 23 |
24 | ) 25 | } 26 | } 27 | 28 | export default DynamicTabContent; 29 | -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import _ from 'lodash'; 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import PropTypes from 'prop-types'; 7 | import getMuiTheme from 'material-ui/styles/getMuiTheme'; 8 | import {Dialog, FlatButton, Menu, MenuItem, TextField} from 'material-ui'; 9 | import {Tabs, Tab} from '../lib/index.js'; 10 | 11 | import DynamicTabContent from './DynamicTabContent'; 12 | import DynamicTabBadge from './DynamicTabBadge'; 13 | 14 | //allow react dev tools work 15 | window.React = React; 16 | 17 | const tabsClassNames = { 18 | tabWrapper: 'myWrapper', 19 | tabBar: 'myTabBar', 20 | tabBarAfter: 'myTabBarAfter', 21 | tab: 'myTab', 22 | tabTitle: 'myTabTitle', 23 | tabCloseIcon: 'tabCloseIcon', 24 | tabBefore: 'myTabBefore', 25 | tabAfter: 'myTabAfter' 26 | }; 27 | 28 | const tabsStyles = { 29 | tabWrapper: {marginTop: '10px'}, 30 | tabBar: {}, 31 | tab:{}, 32 | tabTitle: {}, 33 | tabCloseIcon: {}, 34 | tabBefore: {}, 35 | tabAfter: {} 36 | }; 37 | 38 | class App extends React.Component { 39 | constructor(props) { 40 | super(props); 41 | 42 | let icon = (); 43 | let fonticon = (); 44 | let badge = (); 45 | 46 | this.state = { 47 | tabs:[ 48 | ( 49 |
50 |

This tab cannot close

51 |
52 |
), 53 | ( 54 |
55 |

This is tab1

56 |
57 |
), 58 | ( 59 |
60 |
Lorem ipsum dolor sit amet, consectetur adipisicing elit,
 61 |             
62 |
63 |
), 64 | ( 65 | 66 | ), 67 | ( 68 |
69 |

This is tab4 with custom container style

70 |
71 |
), 72 | ( 73 |
74 |

Super big content

75 | {Array(10000).fill(0).map((_, i) =>

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

)} 76 |
77 |
), 78 | ( 79 |
80 |
Lorem ipsum dolor sit amet, consectetur adipisicing elit,
 81 |             
82 |
83 |
), 84 | ], 85 | badgeCount: 0, 86 | menuPosition: {}, 87 | showMenu: false, 88 | dialogOpen: false 89 | }; 90 | } 91 | 92 | getChildContext() { 93 | return { muiTheme: getMuiTheme()}; 94 | } 95 | 96 | // getChildContext() { 97 | // return { 98 | // muiTheme: ThemeManager.getMuiTheme() 99 | // }; 100 | // } 101 | 102 | makeListeners(key){ 103 | return { 104 | onClick: (e) => { console.log('onClick', key, e);}, // never called 105 | onContextMenu: (e) => { console.log('onContextMenu', key, e); this.handleTabContextMenu(key, e)}, 106 | onDoubleClick: (e) => { console.log('onDoubleClick', key, e); this.handleTabDoubleClick(key, e)}, 107 | } 108 | } 109 | 110 | handleTabSelect(e, key, currentTabs) { 111 | console.log('handleTabSelect key:', key); 112 | this.setState({selectedTab: key, tabs: currentTabs}); 113 | } 114 | 115 | handleTabClose(e, key, currentTabs) { 116 | console.log('tabClosed key:', key); 117 | this.setState({tabs: currentTabs}); 118 | } 119 | 120 | handleTabPositionChange(e, key, currentTabs) { 121 | console.log('tabPositionChanged key:', key); 122 | this.setState({tabs: currentTabs}); 123 | } 124 | 125 | handleTabAddButtonClick(e, currentTabs) { 126 | // key must be unique 127 | const key = 'newTab_' + Date.now(); 128 | let newTab = ( 129 |
130 |

New Empty Tab

131 |
132 |
); 133 | let newTabs = currentTabs.concat([newTab]); 134 | 135 | this.setState({ 136 | tabs: newTabs, 137 | selectedTab: key 138 | }); 139 | } 140 | 141 | handleTabDoubleClick(key) { 142 | this.setState({ 143 | editTabKey: key, 144 | dialogOpen: true 145 | }); 146 | } 147 | 148 | handleTabContextMenu(key, e) { 149 | e.preventDefault(); 150 | this.setState({ 151 | showMenu: true, 152 | contextTarget: key, 153 | menuPosition: { 154 | top:`${e.pageY}px`, 155 | left:`${e.pageX}px` 156 | } 157 | }); 158 | } 159 | 160 | cancelContextMenu(){ 161 | this.setState({ 162 | showMenu: false, 163 | contextTarget: null 164 | }); 165 | } 166 | 167 | renameFromContextMenu(){ 168 | this.setState({ 169 | showMenu: false, 170 | contextTarget: null, 171 | editTabKey: this.state.contextTarget, 172 | dialogOpen: true 173 | }); 174 | } 175 | 176 | closeFromContextMenu(){ 177 | let newTabs = _.filter(this.state.tabs, (t) => {return t.key !== this.state.contextTarget;}); 178 | this.setState({ 179 | showMenu: false, 180 | contextTarget: null, 181 | selectedTab: 'tab0', 182 | tabs: newTabs 183 | }); 184 | } 185 | 186 | _onDialogSubmit() { 187 | const replaceFunc = _.bind((tab) => { 188 | if (tab.key === this.state.editTabKey) { 189 | return React.cloneElement(tab, {title: this.refs.input.getValue()}); 190 | } 191 | return tab; 192 | }, this), 193 | newTabs = _.map(this.state.tabs, replaceFunc); 194 | this.setState({ 195 | tabs: newTabs, 196 | dialogOpen: false 197 | }); 198 | } 199 | 200 | _onDialogCancel() { 201 | this.setState({ 202 | dialogOpen: false 203 | }) 204 | } 205 | 206 | _onDialogShow() { 207 | let tab = _.find(this.state.tabs, (t) => { 208 | return t.key === this.state.editTabKey; 209 | }); 210 | this.refs.input.setValue(tab.props.title); 211 | } 212 | 213 | shouldTabClose(e, key){ 214 | console.log('should tab close', e, key); 215 | return window.confirm('close?'); 216 | } 217 | 218 | render() { 219 | 220 | let standardActions = [ 221 | , 226 | 232 | ]; 233 | 234 | let menuStyle = { 235 | display: this.state.showMenu ? 'block': 'none', 236 | position: 'absolute', 237 | top: this.state.menuPosition.top, 238 | left: this.state.menuPosition.left, 239 | backgroundColor: '#F0F0F0' 240 | }; 241 | 242 | return ( 243 |
244 | 264 |
265 | 266 | {this.state.contextTarget === 'tab0' ? '' : } 267 | 268 | 269 | 270 |
271 | 278 | 280 | 281 |

282 | Source code can be found at GitHub 283 |

284 |
285 | ); 286 | } 287 | } 288 | 289 | App.childContextTypes = { 290 | muiTheme: PropTypes.object 291 | }; 292 | 293 | ReactDOM.render(, document.getElementById('tabs')); 294 | -------------------------------------------------------------------------------- /example/icomoon/Read Me.txt: -------------------------------------------------------------------------------- 1 | Open *demo.html* to see a list of all the glyphs in your font along with their codes/ligatures. 2 | 3 | You won't need any of the files located under the *demo-files* directory when including the generated font in your own projects. 4 | 5 | You can import *selection.json* back to the IcoMoon app using the *Import Icons* button (or via Main Menu > Manage Projects) to retrieve your icon selection. 6 | -------------------------------------------------------------------------------- /example/icomoon/fonts/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgeOsdDev/react-draggable-tab/1f16ea4c8085822f1da7b19bba91f055ce2fe0a1/example/icomoon/fonts/icomoon.eot -------------------------------------------------------------------------------- /example/icomoon/fonts/icomoon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/icomoon/fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgeOsdDev/react-draggable-tab/1f16ea4c8085822f1da7b19bba91f055ce2fe0a1/example/icomoon/fonts/icomoon.ttf -------------------------------------------------------------------------------- /example/icomoon/fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgeOsdDev/react-draggable-tab/1f16ea4c8085822f1da7b19bba91f055ce2fe0a1/example/icomoon/fonts/icomoon.woff -------------------------------------------------------------------------------- /example/icomoon/selection.json: -------------------------------------------------------------------------------- 1 | { 2 | "IcoMoonType": "selection", 3 | "icons": [ 4 | { 5 | "icon": { 6 | "paths": [ 7 | "M60.538 0l82.144 921.63 368.756 102.37 369.724-102.524 82.3-921.476h-902.924zM784.63 301.428h-432.54l10.302 115.75h411.968l-31.042 347.010-231.844 64.254-231.572-64.254-15.83-177.512h113.494l8.048 90.232 125.862 33.916 0.278-0.078 125.934-33.992 13.070-146.55h-391.74l-30.494-341.8h566.214l-10.108 113.024z" 8 | ], 9 | "attrs": [], 10 | "tags": [ 11 | "html5", 12 | "w3c" 13 | ], 14 | "defaultCode": 58602, 15 | "grid": 16 16 | }, 17 | "attrs": [], 18 | "properties": { 19 | "id": 1384, 20 | "order": 2, 21 | "prevSize": 32, 22 | "code": 60127, 23 | "ligatures": "html5, w3c", 24 | "name": "html5" 25 | }, 26 | "setIdx": 0, 27 | "setId": 0, 28 | "iconIdx": 479 29 | } 30 | ], 31 | "height": 1024, 32 | "metadata": { 33 | "name": "icomoon" 34 | }, 35 | "preferences": { 36 | "showGlyphs": true, 37 | "showQuickUse": true, 38 | "showQuickUse2": true, 39 | "showSVGs": true, 40 | "fontPref": { 41 | "prefix": "icon-", 42 | "metadata": { 43 | "fontFamily": "icomoon" 44 | }, 45 | "metrics": { 46 | "emSize": 1024, 47 | "baseline": 6.25, 48 | "whitespace": 50 49 | } 50 | }, 51 | "imagePref": { 52 | "prefix": "icon-", 53 | "png": true, 54 | "useClassSelector": true, 55 | "color": 4473924, 56 | "bgColor": 16777215 57 | }, 58 | "historySize": 100, 59 | "showCodes": false 60 | } 61 | } -------------------------------------------------------------------------------- /example/icomoon/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'icomoon'; 3 | src:url('fonts/icomoon.eot?-8e8w01'); 4 | src:url('fonts/icomoon.eot?#iefix-8e8w01') format('embedded-opentype'), 5 | url('fonts/icomoon.ttf?-8e8w01') format('truetype'), 6 | url('fonts/icomoon.woff?-8e8w01') format('woff'), 7 | url('fonts/icomoon.svg?-8e8w01#icomoon') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | [class^="icon-"], [class*=" icon-"] { 13 | font-family: 'icomoon'; 14 | speak: none; 15 | font-style: normal; 16 | font-weight: normal; 17 | font-variant: normal; 18 | text-transform: none; 19 | line-height: 1; 20 | 21 | /* Better Font Rendering =========== */ 22 | -webkit-font-smoothing: antialiased; 23 | -moz-osx-font-smoothing: grayscale; 24 | } 25 | 26 | .icon-html5:before { 27 | content: "\eadf"; 28 | } 29 | -------------------------------------------------------------------------------- /example/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgeOsdDev/react-draggable-tab/1f16ea4c8085822f1da7b19bba91f055ce2fe0a1/example/icon.png -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React draggable tab example 6 | 7 | 8 | 13 | 14 | 15 | Fork me on GitHub 16 |
17 | 30 | 31 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Fri May 15 2015 12:31:20 GMT+0900 (JST) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['browserify', 'mocha'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | 'test/**/*_spec.js' 19 | ], 20 | 21 | 22 | // list of files to exclude 23 | exclude: [ 24 | 'test/_helper/*.js' 25 | ], 26 | 27 | 28 | // preprocess matching files before serving them to the browser 29 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 30 | preprocessors: { 31 | 'test/**/*_spec.js': ['browserify'] 32 | }, 33 | 34 | browserify: { 35 | debug: true, 36 | transform: [ 37 | [ 38 | 'babelify', { 39 | 'presets': [ 40 | 'es2015', 41 | 'stage-2', 42 | 'react' 43 | ] 44 | } 45 | ] 46 | ] 47 | }, 48 | 49 | // test results reporter to use 50 | // possible values: 'dots', 'progress' 51 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 52 | reporters: ['spec'], 53 | 54 | // client: { 55 | // mocha: { 56 | // // reporter: 'html', // change Karma's debug.html to the mocha web reporter 57 | // // ui: 'tdd' 58 | // } 59 | // }, 60 | 61 | // web server port 62 | port: 9876, 63 | 64 | 65 | // enable / disable colors in the output (reporters and logs) 66 | colors: true, 67 | 68 | 69 | // level of logging 70 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 71 | logLevel: config.LOG_INFO, 72 | 73 | 74 | // enable / disable watching file and executing tests whenever any file changes 75 | autoWatch: true, 76 | 77 | 78 | // start these browsers 79 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 80 | // browsers: ['Chrome', 'Firefox', 'PhantomJS', 'IE'], 81 | browsers: [process.env.CONTINUOUS_INTEGRATION ? 'Firefox' : 'Chrome'], 82 | 83 | 84 | // Continuous Integration mode 85 | // if true, Karma captures browsers, runs the tests and exits 86 | singleRun: false 87 | }); 88 | }; 89 | -------------------------------------------------------------------------------- /lib/components/CloseIcon.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _react = require('react'); 10 | 11 | var _react2 = _interopRequireDefault(_react); 12 | 13 | var _propTypes = require('prop-types'); 14 | 15 | var _propTypes2 = _interopRequireDefault(_propTypes); 16 | 17 | var _classnames = require('classnames'); 18 | 19 | var _classnames2 = _interopRequireDefault(_classnames); 20 | 21 | var _TabStyles = require('./TabStyles'); 22 | 23 | var _TabStyles2 = _interopRequireDefault(_TabStyles); 24 | 25 | var _styleOverride = require('../helpers/styleOverride'); 26 | 27 | var _styleOverride2 = _interopRequireDefault(_styleOverride); 28 | 29 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 30 | 31 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 32 | 33 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 34 | 35 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 36 | 37 | var CloseIcon = function (_React$Component) { 38 | _inherits(CloseIcon, _React$Component); 39 | 40 | function CloseIcon(props) { 41 | _classCallCheck(this, CloseIcon); 42 | 43 | var _this = _possibleConstructorReturn(this, (CloseIcon.__proto__ || Object.getPrototypeOf(CloseIcon)).call(this, props)); 44 | 45 | _this.state = { 46 | hover: false 47 | }; 48 | return _this; 49 | } 50 | 51 | _createClass(CloseIcon, [{ 52 | key: 'handleMouseEnter', 53 | value: function handleMouseEnter() { 54 | this.setState({ hover: true }); 55 | } 56 | }, { 57 | key: 'handleMouseLeave', 58 | value: function handleMouseLeave() { 59 | this.setState({ hover: false }); 60 | } 61 | }, { 62 | key: 'handleClick', 63 | value: function handleClick(e) { 64 | this.props.onClick(e); 65 | } 66 | }, { 67 | key: 'render', 68 | value: function render() { 69 | var iconStyle = this.props.style; 70 | var className = this.props.className; 71 | 72 | if (this.state.hover) { 73 | iconStyle = _styleOverride2.default.merge(this.props.style, _styleOverride2.default.merge(_TabStyles2.default.tabCloseIconOnHover, this.props.hoverStyle)); 74 | className = (0, _classnames2.default)(this.props.className, 'hover'); 75 | } else { 76 | iconStyle = this.props.style; 77 | } 78 | 79 | return _react2.default.createElement( 80 | 'span', 81 | { 82 | style: iconStyle, 83 | className: className, 84 | onMouseEnter: this.handleMouseEnter.bind(this), 85 | onMouseLeave: this.handleMouseLeave.bind(this), 86 | onClick: this.handleClick.bind(this) }, 87 | this.props.children 88 | ); 89 | } 90 | }]); 91 | 92 | return CloseIcon; 93 | }(_react2.default.Component); 94 | 95 | CloseIcon.defaultProps = { 96 | style: {}, 97 | hoverStyle: {}, 98 | onClick: function onClick() {} 99 | }; 100 | 101 | CloseIcon.propTypes = { 102 | style: _propTypes2.default.object, 103 | hoverStyle: _propTypes2.default.object, 104 | onClick: _propTypes2.default.func 105 | }; 106 | 107 | exports.default = CloseIcon; -------------------------------------------------------------------------------- /lib/components/CustomDraggable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _reactDraggable = require('react-draggable'); 10 | 11 | var _reactDraggable2 = _interopRequireDefault(_reactDraggable); 12 | 13 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 14 | 15 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 16 | 17 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 18 | 19 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 20 | 21 | var CustomDraggable = function (_Draggable) { 22 | _inherits(CustomDraggable, _Draggable); 23 | 24 | function CustomDraggable() { 25 | _classCallCheck(this, CustomDraggable); 26 | 27 | return _possibleConstructorReturn(this, (CustomDraggable.__proto__ || Object.getPrototypeOf(CustomDraggable)).apply(this, arguments)); 28 | } 29 | 30 | _createClass(CustomDraggable, [{ 31 | key: 'componentWillReceiveProps', 32 | value: function componentWillReceiveProps(nextProps) { 33 | var newState = { 34 | clientX: nextProps.start.x, 35 | clientY: nextProps.start.y 36 | }; 37 | this.setState(newState); 38 | } 39 | }]); 40 | 41 | return CustomDraggable; 42 | }(_reactDraggable2.default); 43 | 44 | exports.default = CustomDraggable; -------------------------------------------------------------------------------- /lib/components/Tab.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _react = require('react'); 10 | 11 | var _react2 = _interopRequireDefault(_react); 12 | 13 | var _propTypes = require('prop-types'); 14 | 15 | var _propTypes2 = _interopRequireDefault(_propTypes); 16 | 17 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 18 | 19 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 20 | 21 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 22 | 23 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 24 | 25 | var Tab = function (_React$Component) { 26 | _inherits(Tab, _React$Component); 27 | 28 | function Tab(props) { 29 | _classCallCheck(this, Tab); 30 | 31 | var _this = _possibleConstructorReturn(this, (Tab.__proto__ || Object.getPrototypeOf(Tab)).call(this, props)); 32 | 33 | _this.state = {}; 34 | return _this; 35 | } 36 | 37 | _createClass(Tab, [{ 38 | key: 'render', 39 | value: function render() { 40 | return this.props.children; 41 | } 42 | }]); 43 | 44 | return Tab; 45 | }(_react2.default.Component); 46 | 47 | Tab.defaultProps = { 48 | beforeTitle: _react2.default.createElement('span', null), 49 | title: 'untitled', 50 | afterTitle: _react2.default.createElement('span', null), 51 | unclosable: false, 52 | tabClassNames: { 53 | tab: '', 54 | tabBefore: '', 55 | tabAfter: '', 56 | tabTitle: '', 57 | tabBeforeTitle: '', 58 | tabAfterTitle: '', 59 | tabCloseIcon: '', 60 | tabActive: '', 61 | tabHover: '' 62 | }, 63 | tabStyles: {}, 64 | containerStyle: {} 65 | }; 66 | 67 | Tab.propTypes = { 68 | beforeTitle: _propTypes2.default.element, 69 | title: _propTypes2.default.oneOfType([_propTypes2.default.string, _propTypes2.default.element]).isRequired, 70 | afterTitle: _propTypes2.default.element, 71 | unclosable: _propTypes2.default.bool, 72 | tabClassNames: _propTypes2.default.shape({ 73 | tab: _propTypes2.default.string, 74 | tabBefore: _propTypes2.default.string, 75 | tabAfter: _propTypes2.default.string, 76 | tabBeforeTitle: _propTypes2.default.string, 77 | tabTitle: _propTypes2.default.string, 78 | tabAfterTitle: _propTypes2.default.string, 79 | tabCloseIcon: _propTypes2.default.string, 80 | tabActive: _propTypes2.default.string, 81 | tabHover: _propTypes2.default.string 82 | }), 83 | tabStyles: _propTypes2.default.shape({ 84 | tab: _propTypes2.default.object, 85 | tabBefore: _propTypes2.default.object, 86 | tabAfter: _propTypes2.default.object, 87 | tabTitle: _propTypes2.default.object, 88 | tabActive: _propTypes2.default.object, 89 | tabTitleActive: _propTypes2.default.object, 90 | tabBeforeActive: _propTypes2.default.object, 91 | tabAfterActive: _propTypes2.default.object, 92 | tabOnHover: _propTypes2.default.object, 93 | tabTitleOnHover: _propTypes2.default.object, 94 | tabBeforeOnHover: _propTypes2.default.object, 95 | tabAfterOnHover: _propTypes2.default.object, 96 | tabCloseIcon: _propTypes2.default.object, 97 | tabCloseIconHover: _propTypes2.default.object 98 | }), 99 | containerStyle: _propTypes2.default.object, 100 | hiddenContainerStyle: _propTypes2.default.object 101 | }; 102 | 103 | exports.default = Tab; -------------------------------------------------------------------------------- /lib/components/TabContainer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _react = require('react'); 10 | 11 | var _react2 = _interopRequireDefault(_react); 12 | 13 | var _propTypes = require('prop-types'); 14 | 15 | var _propTypes2 = _interopRequireDefault(_propTypes); 16 | 17 | var _styleOverride = require('../helpers/styleOverride'); 18 | 19 | var _styleOverride2 = _interopRequireDefault(_styleOverride); 20 | 21 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 22 | 23 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 24 | 25 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 26 | 27 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 28 | 29 | var styles = { 30 | root: { 31 | width: '100%', 32 | position: 'relative', 33 | textAlign: 'initial' 34 | } 35 | }; 36 | 37 | var TabContainer = function (_React$Component) { 38 | _inherits(TabContainer, _React$Component); 39 | 40 | function TabContainer() { 41 | _classCallCheck(this, TabContainer); 42 | 43 | return _possibleConstructorReturn(this, (TabContainer.__proto__ || Object.getPrototypeOf(TabContainer)).apply(this, arguments)); 44 | } 45 | 46 | _createClass(TabContainer, [{ 47 | key: 'render', 48 | value: function render() { 49 | var style = _styleOverride2.default.merge(styles.root, this.props.style); 50 | if (!this.props.selected) { 51 | style = _styleOverride2.default.merge(styles.root, this.props.hiddenStyle); 52 | } 53 | return _react2.default.createElement( 54 | 'div', 55 | { style: style }, 56 | this.props.children 57 | ); 58 | } 59 | }]); 60 | 61 | return TabContainer; 62 | }(_react2.default.Component); 63 | 64 | TabContainer.defaultProps = { 65 | selected: false, 66 | style: {}, 67 | hiddenStyle: { 68 | height: '0px', 69 | overflow: 'hidden' 70 | } 71 | }; 72 | 73 | TabContainer.propTypes = { 74 | selected: _propTypes2.default.bool.isRequired, 75 | style: _propTypes2.default.object, 76 | hiddenStyle: _propTypes2.default.object 77 | }; 78 | 79 | exports.default = TabContainer; -------------------------------------------------------------------------------- /lib/components/TabStyles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | /* Inspired from Atom 7 | https://github.com/atom/tabs 8 | https://github.com/atom/atom-dark-ui 9 | */ 10 | var TabStyles = { 11 | 12 | tabWrapper: { 13 | height: '100%', 14 | width: '100%', 15 | position: 'relative' 16 | }, 17 | 18 | tabBar: { 19 | // @TODO safari needs prefix. Style should be define in CSS. 20 | // Can't use duprecated key's for inline-style. 21 | // See https://github.com/facebook/react/issues/2020 22 | // display: '-webkit-flex', 23 | // display: '-ms-flexbox', 24 | display: 'flex', 25 | WebkitUserSelect: 'none', 26 | MozUserSelect: 'none', 27 | msUserSelect: 'none', 28 | userSelect: 'none', 29 | margin: 0, 30 | listStyle: 'none', 31 | outline: '0px', 32 | overflowY: 'hidden', 33 | overflowX: 'hidden', 34 | minWidth: '95%', 35 | maxWidth: '99%', 36 | paddingRight: '35px' 37 | }, 38 | 39 | tabBarAfter: { 40 | content: '', 41 | position: 'absolute', 42 | top: '26px', 43 | height: '5px', 44 | left: 0, 45 | right: 0, 46 | zIndex: 2, 47 | backgroundColor: '#222222', 48 | borderBottom: '1px solid #111111', 49 | pointerEvents: 'none' 50 | }, 51 | 52 | tab: { 53 | fontFamily: "'Lucida Grande', 'Segoe UI', Ubuntu, Cantarell, sans-serif", 54 | backgroundImage: 'linear-gradient(#454545, #333333)', 55 | height: '26px', 56 | fontSize: '11px', 57 | position: 'relative', 58 | marginLeft: '30px', 59 | paddingLeft: '15px', 60 | paddingRight: '24px', 61 | WebkutBoxFlex: 1, 62 | WebkitFlex: 1, 63 | MozFlex: 1, 64 | msFlex: 1, 65 | flex: 1, 66 | maxWidth: '175px', 67 | minWidth: '0px', 68 | transform: 'translate(0px, 0px)' 69 | }, 70 | 71 | tabBefore: { 72 | content: '', 73 | position: 'absolute', 74 | top: '0px', 75 | width: '25px', 76 | height: '26px', 77 | 78 | left: '-14px', 79 | borderTopLeftRadius: '3px', 80 | boxShadow: 'inset 1px 1px 0 #484848, -4px 0px 4px rgba(0, 0, 0, 0.1)', 81 | WebkitTransform: 'skewX(-30deg)', 82 | MozTransform: 'skewX(-30deg)', 83 | msTransform: 'skewX(-30deg)', 84 | transform: 'skewX(-30deg)', 85 | backgroundImage: 'linear-gradient(#454545, #333333)', 86 | borderRadius: '7.5px 0 0 0' 87 | }, 88 | 89 | tabAfter: { 90 | content: '', 91 | position: 'absolute', 92 | top: '0px', 93 | width: '25px', 94 | height: '26px', 95 | 96 | right: '-14px', 97 | borderTopRightRadius: '3px', 98 | boxShadow: 'inset -1px 1px 0 #484848, 4px 0px 4px rgba(0, 0, 0, 0.1)', 99 | WebkitTransform: 'skewX(30deg)', 100 | MozTransform: 'skewX(30deg)', 101 | msTransform: 'skewX(30deg)', 102 | transform: 'skewX(30deg)', 103 | backgroundImage: 'linear-gradient(#454545, #333333)', 104 | borderRadius: '0 7.5px 0 0' 105 | }, 106 | 107 | tabTitle: { 108 | cursor: 'default', 109 | overflow: 'hidden', 110 | whiteSpace: 'nowrap', 111 | textOverflow: 'ellipsis', 112 | marginTop: '8px', 113 | float: 'left', 114 | textAlign: 'center', 115 | postion: 'relative', 116 | width: '90%', 117 | color: 'rgb(170, 170, 170)' 118 | }, 119 | 120 | tabActive: { 121 | WebkutBoxFlex: 2, 122 | WebkitFlex: 2, 123 | MozFlex: 2, 124 | msFlex: 2, 125 | flex: 2, 126 | zIndex: 1, 127 | color: '#ffffff', 128 | fontSize: '13px', 129 | backgroundImage: 'linear-gradient(#343434, #222222)' 130 | }, 131 | 132 | tabBeforeActive: { 133 | backgroundImage: 'linear-gradient(#343434, #222222)' 134 | }, 135 | 136 | tabAfterActive: { 137 | backgroundImage: 'linear-gradient(#343434, #222222)' 138 | }, 139 | 140 | tabTitleActive: { 141 | lineHeight: '1.5em', 142 | color: 'rgb(255, 255, 255)', 143 | marginTop: '6px' 144 | }, 145 | 146 | tabOnHover: { 147 | backgroundImage: 'linear-gradient(#333333, #222222)' 148 | }, 149 | 150 | tabBeforeOnHover: { 151 | backgroundImage: 'linear-gradient(#333333, #222222)' 152 | }, 153 | 154 | tabAfterOnHover: { 155 | backgroundImage: 'linear-gradient(#333333, #222222)' 156 | }, 157 | 158 | tabTitleOnHover: { 159 | filter: 'alpha(opacity=20)' 160 | }, 161 | 162 | tabCloseIcon: { 163 | position: 'absolute', 164 | cursor: 'pointer', 165 | font: '16px arial, sans-serif', 166 | right: '-2px', 167 | marginTop: '8px', 168 | textDecoration: 'none', 169 | textShadow: '0 1px 0 #fff', 170 | lineHeight: '1em', 171 | filter: 'alpha(opacity=20)', 172 | opacity: '.2', 173 | width: '16px', 174 | height: '16px', 175 | textAlign: 'center', 176 | WebkitBorderRadius: '8px', 177 | MozBorderRadius: '8px', 178 | borderRadius: '8px', 179 | zIndex: 999 180 | }, 181 | 182 | tabCloseIconOnHover: { 183 | filter: 'none', 184 | backgroundColor: 'red', 185 | color: 'white' 186 | }, 187 | 188 | tabAddButton: { 189 | cursor: 'pointer', 190 | fontFamily: "'Lucida Grande', 'Segoe UI', Ubuntu, Cantarell, sans-serif", 191 | fontSize: '20px', 192 | textShadow: 'rgb(255, 255, 255) 0px 1px 0px', 193 | position: 'relative', 194 | width: '25px', 195 | height: '26px', 196 | marginLeft: '20px', 197 | zIndex: 2 198 | }, 199 | 200 | beforeTitle: { 201 | position: 'absolute', 202 | top: '8px', 203 | left: '-8px', 204 | zIndex: 2 205 | }, 206 | 207 | afterTitle: { 208 | position: 'absolute', 209 | top: '8px', 210 | right: '16px', 211 | zIndex: 2 212 | } 213 | }; 214 | 215 | exports.default = TabStyles; -------------------------------------------------------------------------------- /lib/components/Tabs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 10 | 11 | var _react = require('react'); 12 | 13 | var _react2 = _interopRequireDefault(_react); 14 | 15 | var _reactDom = require('react-dom'); 16 | 17 | var _reactDom2 = _interopRequireDefault(_reactDom); 18 | 19 | var _propTypes = require('prop-types'); 20 | 21 | var _propTypes2 = _interopRequireDefault(_propTypes); 22 | 23 | var _lodash = require('lodash'); 24 | 25 | var _lodash2 = _interopRequireDefault(_lodash); 26 | 27 | var _invariant = require('invariant'); 28 | 29 | var _invariant2 = _interopRequireDefault(_invariant); 30 | 31 | var _classnames = require('classnames'); 32 | 33 | var _classnames2 = _interopRequireDefault(_classnames); 34 | 35 | var _mousetrap = require('mousetrap'); 36 | 37 | var _mousetrap2 = _interopRequireDefault(_mousetrap); 38 | 39 | var _CustomDraggable = require('./CustomDraggable'); 40 | 41 | var _CustomDraggable2 = _interopRequireDefault(_CustomDraggable); 42 | 43 | var _TabStyles = require('./TabStyles'); 44 | 45 | var _TabStyles2 = _interopRequireDefault(_TabStyles); 46 | 47 | var _TabContainer = require('./TabContainer'); 48 | 49 | var _TabContainer2 = _interopRequireDefault(_TabContainer); 50 | 51 | var _CloseIcon = require('./CloseIcon'); 52 | 53 | var _CloseIcon2 = _interopRequireDefault(_CloseIcon); 54 | 55 | var _styleOverride = require('../helpers/styleOverride'); 56 | 57 | var _styleOverride2 = _interopRequireDefault(_styleOverride); 58 | 59 | var _utils = require('../helpers/utils'); 60 | 61 | var _utils2 = _interopRequireDefault(_utils); 62 | 63 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 64 | 65 | function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } 66 | 67 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 68 | 69 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 70 | 71 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 72 | 73 | function tabStateFromProps(props) { 74 | var tabs = []; 75 | var idx = 0; 76 | _react2.default.Children.forEach(props.tabs, function (tab) { 77 | (0, _invariant2.default)(tab.key, 'There should be unique key in each Tab'); 78 | 79 | tabs[idx] = tab; 80 | idx += 1; 81 | }); 82 | return { tabs: tabs }; 83 | } 84 | 85 | var Tabs = function (_React$Component) { 86 | _inherits(Tabs, _React$Component); 87 | 88 | function Tabs(props) { 89 | _classCallCheck(this, Tabs); 90 | 91 | var _this = _possibleConstructorReturn(this, (Tabs.__proto__ || Object.getPrototypeOf(Tabs)).call(this, props)); 92 | 93 | var _tabStateFromProps = tabStateFromProps(_this.props), 94 | tabs = _tabStateFromProps.tabs; 95 | 96 | var selectedTab = ''; 97 | if (_this.props.selectedTab) { 98 | selectedTab = _this.props.selectedTab; // eslint-disable-line prefer-destructuring 99 | } else if (_this.props.tabs) { 100 | selectedTab = _this.props.tabs[0].key; 101 | } 102 | 103 | _this.state = { 104 | tabs: tabs, 105 | selectedTab: selectedTab, 106 | hoveredTab: '', 107 | closedTabs: [] 108 | }; 109 | 110 | // Dom positons 111 | // do not save in state 112 | _this.startPositions = []; 113 | _this.dragging = false; 114 | return _this; 115 | } 116 | 117 | _createClass(Tabs, [{ 118 | key: 'isClosed', 119 | value: function isClosed(key) { 120 | return this.state.closedTabs.indexOf(key) > -1; 121 | } 122 | }, { 123 | key: 'getIndexOfTabByKey', 124 | value: function getIndexOfTabByKey(key) { 125 | return _lodash2.default.findIndex(this.state.tabs, function (tab) { 126 | return tab.key === key; 127 | }); 128 | } 129 | }, { 130 | key: 'getNextTabKey', 131 | value: function getNextTabKey(key) { 132 | var nextKey = void 0; 133 | var current = this.getIndexOfTabByKey(key); 134 | if (current + 1 < this.state.tabs.length) { 135 | nextKey = this.state.tabs[current + 1].key; 136 | if (this.isClosed(nextKey)) { 137 | nextKey = this.getNextTabKey(nextKey); 138 | } 139 | } 140 | return nextKey; 141 | } 142 | }, { 143 | key: 'getPrevTabKey', 144 | value: function getPrevTabKey(key) { 145 | var prevKey = void 0; 146 | var current = this.getIndexOfTabByKey(key); 147 | if (current > 0) { 148 | prevKey = this.state.tabs[current - 1].key; 149 | if (this.isClosed(prevKey)) { 150 | prevKey = this.getPrevTabKey(prevKey); 151 | } 152 | } 153 | return prevKey; 154 | } 155 | }, { 156 | key: 'getCurrentOpenTabs', 157 | value: function getCurrentOpenTabs() { 158 | return this.getOpenTabs(this.state.tabs); 159 | } 160 | }, { 161 | key: 'getOpenTabs', 162 | value: function getOpenTabs(tabs) { 163 | var _this2 = this; 164 | 165 | return _lodash2.default.filter(tabs, function (tab) { 166 | return !_this2.isClosed(tab.key); 167 | }); 168 | } 169 | }, { 170 | key: 'moveTabPosition', 171 | value: function moveTabPosition(key1, key2) { 172 | var t1 = this.getIndexOfTabByKey(key1); 173 | var t2 = this.getIndexOfTabByKey(key2); 174 | return _utils2.default.slideArray(this.state.tabs, t1, t2); 175 | } 176 | }, { 177 | key: 'saveStartPositions', 178 | value: function saveStartPositions() { 179 | var _this3 = this; 180 | 181 | // Do not save in state 182 | this.startPositions = _lodash2.default.map(this.state.tabs, function (tab) { 183 | var el = _reactDom2.default.findDOMNode(_this3.refs[tab.key]); 184 | var pos = el ? el.getBoundingClientRect() : {}; 185 | return { 186 | key: tab.key, 187 | pos: pos 188 | }; 189 | }); 190 | } 191 | 192 | // eslint-disable-next-line class-methods-use-this 193 | 194 | }, { 195 | key: 'cancelEventSafety', 196 | value: function cancelEventSafety(e) { 197 | var ev = e; 198 | if (typeof e.preventDefault !== 'function') { 199 | ev.preventDefault = function () {}; 200 | } 201 | if (typeof e.stopPropagation !== 'function') { 202 | ev.stopPropagation = function () {}; 203 | } 204 | ev.preventDefault(); 205 | ev.stopPropagation(); 206 | return ev; 207 | } 208 | 209 | // eslint-disable-next-line class-methods-use-this 210 | 211 | }, { 212 | key: 'componentWillMount', 213 | value: function componentWillMount() {} 214 | }, { 215 | key: 'componentDidMount', 216 | value: function componentDidMount() { 217 | this.saveStartPositions(); 218 | this.bindShortcuts(); 219 | } 220 | }, { 221 | key: 'componentWillUnmount', 222 | value: function componentWillUnmount() { 223 | this.constructor.unbindShortcuts(); 224 | } 225 | }, { 226 | key: 'componentWillReceiveProps', 227 | value: function componentWillReceiveProps(nextProps) { 228 | var newState = tabStateFromProps(nextProps); 229 | if (nextProps.selectedTab !== 'undefined') { 230 | newState.selectedTab = nextProps.selectedTab; 231 | } 232 | // reset closedTabs, respect props from application 233 | newState.closedTabs = []; 234 | this.setState(newState); 235 | } 236 | 237 | // eslint-disable-next-line class-methods-use-this 238 | 239 | }, { 240 | key: 'componentWillUpdate', 241 | value: function componentWillUpdate() {} 242 | }, { 243 | key: 'componentDidUpdate', 244 | value: function componentDidUpdate() { 245 | this.saveStartPositions(); 246 | } 247 | }, { 248 | key: 'bindShortcuts', 249 | value: function bindShortcuts() { 250 | var _this4 = this; 251 | 252 | if (this.props.shortCutKeys) { 253 | if (this.props.shortCutKeys.close) { 254 | _mousetrap2.default.bind(this.props.shortCutKeys.close, function (e) { 255 | var ev = _this4.cancelEventSafety(e); 256 | if (_this4.state.selectedTab) { 257 | _this4.handleCloseButtonClick(_this4.state.selectedTab, ev); 258 | } 259 | }); 260 | } 261 | if (this.props.shortCutKeys.create) { 262 | _mousetrap2.default.bind(this.props.shortCutKeys.create, function (e) { 263 | var ev = _this4.cancelEventSafety(e); 264 | _this4.handleAddButtonClick(ev); 265 | }); 266 | } 267 | if (this.props.shortCutKeys.moveRight) { 268 | _mousetrap2.default.bind(this.props.shortCutKeys.moveRight, function (e) { 269 | var ev = _this4.cancelEventSafety(e); 270 | _this4.moveRight(ev); 271 | }); 272 | } 273 | if (this.props.shortCutKeys.moveLeft) { 274 | _mousetrap2.default.bind(this.props.shortCutKeys.moveLeft, function (e) { 275 | var ev = _this4.cancelEventSafety(e); 276 | _this4.moveLeft(ev); 277 | }); 278 | } 279 | } 280 | } 281 | }, { 282 | key: 'handleDragStart', 283 | value: function handleDragStart() { 284 | this.dragging = true; 285 | } 286 | }, { 287 | key: 'handleDrag', 288 | value: function handleDrag(key, e) { 289 | var _this5 = this; 290 | 291 | var deltaX = e.pageX || e.clientX; 292 | _lodash2.default.each(this.startPositions, function (pos) { 293 | var tempMoved = pos.moved || 0; 294 | var shoudBeSwap = key !== pos.key && pos.pos.left + tempMoved < deltaX && deltaX < pos.pos.right + tempMoved; 295 | if (shoudBeSwap) { 296 | var idx1 = _this5.getIndexOfTabByKey(key); 297 | var idx2 = _this5.getIndexOfTabByKey(pos.key); 298 | var minus = idx1 > idx2 ? 1 : -1; 299 | var movePx = minus * (pos.pos.right - pos.pos.left) - tempMoved; 300 | _reactDom2.default.findDOMNode(_this5.refs[pos.key]).style.transform = 'translate(' + movePx + 'px, 0px)'; 301 | _this5.startPositions[idx2].moved = movePx; 302 | } 303 | }); 304 | } 305 | }, { 306 | key: 'handleDragStop', 307 | value: function handleDragStop(key, e) { 308 | var _this6 = this; 309 | 310 | var deltaX = e.pageX || e.clientX; 311 | var swapedTabs = null; 312 | _lodash2.default.each(this.startPositions, function (pos) { 313 | var shoudBeSwap = key !== pos.key && pos.pos.left < deltaX && deltaX < pos.pos.right; 314 | if (shoudBeSwap) { 315 | swapedTabs = _this6.moveTabPosition(key, pos.key); 316 | } 317 | _reactDom2.default.findDOMNode(_this6.refs[pos.key]).style.transform = 'translate(0px, 0px)'; 318 | }); 319 | var nextTabs = swapedTabs || this.state.tabs; 320 | 321 | this.dragging = false; 322 | this.setState({ 323 | tabs: nextTabs, 324 | selectedTab: key 325 | }, function () { 326 | if (swapedTabs) { 327 | _this6.props.onTabPositionChange(e, key, _this6.getOpenTabs(nextTabs)); 328 | } 329 | }); 330 | } 331 | }, { 332 | key: 'handleTabClick', 333 | value: function handleTabClick(key, e) { 334 | var _this7 = this; 335 | 336 | var isBehindTab = key !== this.state.selectedTab; 337 | var idx = this.getIndexOfTabByKey(key); 338 | var isDragAfter = this.startPositions[idx].moved !== 0; 339 | if (isBehindTab && isDragAfter && this.props.keepSelectedTab) { 340 | e.preventDefault(); 341 | return; 342 | } 343 | 344 | var classes = (e.target.getAttribute('class') || '').split(' '); 345 | if (classes.indexOf('rdTabCloseIcon') > -1) { 346 | this.cancelEventSafety(e); 347 | } else { 348 | this.setState({ selectedTab: key }, function () { 349 | _this7.props.onTabSelect(e, key, _this7.getCurrentOpenTabs()); 350 | }); 351 | } 352 | } 353 | }, { 354 | key: 'handleMouseEnter', 355 | value: function handleMouseEnter(key, onMouseEnter, e) { 356 | if (!this.dragging) { 357 | this.setState({ 358 | hoveredTab: key 359 | }, function () { 360 | if (_lodash2.default.isFunction(onMouseEnter)) { 361 | onMouseEnter(e); 362 | } 363 | }); 364 | } 365 | } 366 | }, { 367 | key: 'handleMouseLeave', 368 | value: function handleMouseLeave(key, onMouseLeave, e) { 369 | if (!this.dragging) { 370 | if (this.state.hoveredTab === key) { 371 | this.setState({ 372 | hoveredTab: '' 373 | }, function () { 374 | if (_lodash2.default.isFunction(onMouseLeave)) { 375 | onMouseLeave(e); 376 | } 377 | }); 378 | } else if (_lodash2.default.isFunction(onMouseLeave)) { 379 | onMouseLeave(e); 380 | } 381 | } 382 | } 383 | }, { 384 | key: 'handleCloseButtonClick', 385 | value: function handleCloseButtonClick(key, e) { 386 | var _this8 = this; 387 | 388 | var ev = this.cancelEventSafety(e); 389 | var doClose = function () { 390 | var nextSelected = void 0; 391 | 392 | if (_this8.state.selectedTab === key) { 393 | nextSelected = _this8.getNextTabKey(key); 394 | if (!nextSelected) { 395 | nextSelected = _this8.getPrevTabKey(key); 396 | } 397 | } else { 398 | nextSelected = _this8.state.selectedTab; 399 | } 400 | 401 | var shoudBeNotifyTabChange = _this8.state.selectedTab !== nextSelected; 402 | _this8.setState({ 403 | closedTabs: _this8.state.closedTabs.concat([key]), 404 | selectedTab: nextSelected 405 | }, function () { 406 | var currentOpenTabs = _this8.getCurrentOpenTabs(); 407 | _this8.props.onTabClose(ev, key, currentOpenTabs); 408 | if (shoudBeNotifyTabChange) { 409 | _this8.props.onTabSelect(ev, nextSelected, currentOpenTabs); 410 | } 411 | }); 412 | }; 413 | if (this.props.shouldTabClose(ev, key)) { 414 | doClose(); 415 | } 416 | } 417 | }, { 418 | key: 'handleAddButtonClick', 419 | value: function handleAddButtonClick(e) { 420 | this.props.onTabAddButtonClick(e, this.getCurrentOpenTabs()); 421 | } 422 | }, { 423 | key: 'moveRight', 424 | value: function moveRight(e) { 425 | var _this9 = this; 426 | 427 | var nextSelected = this.getNextTabKey(this.state.selectedTab); 428 | if (!nextSelected) { 429 | nextSelected = this.props.tabs[0] ? this.props.tabs[0].key : ''; 430 | } 431 | if (nextSelected !== this.state.selectedTab) { 432 | this.setState({ selectedTab: nextSelected }, function () { 433 | _this9.props.onTabSelect(e, nextSelected, _this9.getCurrentOpenTabs()); 434 | }); 435 | } 436 | } 437 | }, { 438 | key: 'moveLeft', 439 | value: function moveLeft(e) { 440 | var _this10 = this; 441 | 442 | var nextSelected = this.getPrevTabKey(this.state.selectedTab); 443 | if (!nextSelected) { 444 | nextSelected = _lodash2.default.last(this.props.tabs) ? _lodash2.default.last(this.props.tabs).key : ''; 445 | } 446 | if (nextSelected !== this.state.selectedTab) { 447 | this.setState({ selectedTab: nextSelected }, function () { 448 | _this10.props.onTabSelect(e, nextSelected, _this10.getCurrentOpenTabs()); 449 | }); 450 | } 451 | } 452 | }, { 453 | key: 'getCloseButton', 454 | value: function getCloseButton(tab, style, classes, hoverStyleBase) { 455 | if (tab.props.unclosable) { 456 | return ''; 457 | } 458 | var onHoverStyle = _styleOverride2.default.merge(hoverStyleBase, tab.props.tabStyles.tabCloseIconOnHover); 459 | return _react2.default.createElement( 460 | _CloseIcon2.default, 461 | { 462 | style: style, 463 | hoverStyle: onHoverStyle, 464 | className: classes, 465 | onClick: this.handleCloseButtonClick.bind(this, tab.key) }, 466 | '\xD7' 467 | ); 468 | } 469 | }, { 470 | key: 'render', 471 | value: function render() { 472 | var _this11 = this; 473 | 474 | // override inline tabs styles 475 | var tabInlineStyles = {}; 476 | tabInlineStyles.tabWrapper = _styleOverride2.default.merge(_TabStyles2.default.tabWrapper, this.props.tabsStyles.tabWrapper); 477 | tabInlineStyles.tabBar = _styleOverride2.default.merge(_TabStyles2.default.tabBar, this.props.tabsStyles.tabBar); 478 | tabInlineStyles.tabBarAfter = _styleOverride2.default.merge(_TabStyles2.default.tabBarAfter, this.props.tabsStyles.tabBarAfter); 479 | tabInlineStyles.tab = _styleOverride2.default.merge(_TabStyles2.default.tab, this.props.tabsStyles.tab); 480 | tabInlineStyles.tabBefore = _styleOverride2.default.merge(_TabStyles2.default.tabBefore, this.props.tabsStyles.tabBefore); 481 | tabInlineStyles.tabAfter = _styleOverride2.default.merge(_TabStyles2.default.tabAfter, this.props.tabsStyles.tabAfter); 482 | tabInlineStyles.tabTitle = _styleOverride2.default.merge(_TabStyles2.default.tabTitle, this.props.tabsStyles.tabTitle); 483 | tabInlineStyles.tabCloseIcon = _styleOverride2.default.merge(_TabStyles2.default.tabCloseIcon, this.props.tabsStyles.tabCloseIcon); 484 | tabInlineStyles.tabCloseIconOnHover = _styleOverride2.default.merge(_TabStyles2.default.tabCloseIconOnHover, this.props.tabsStyles.tabCloseIconOnHover); 485 | 486 | tabInlineStyles.tabActive = _styleOverride2.default.merge(_TabStyles2.default.tabActive, this.props.tabsStyles.tabActive); 487 | tabInlineStyles.tabTitleActive = _styleOverride2.default.merge(_TabStyles2.default.tabTitleActive, this.props.tabsStyles.tabTitleActive); 488 | tabInlineStyles.tabBeforeActive = _styleOverride2.default.merge(_TabStyles2.default.tabBeforeActive, this.props.tabsStyles.tabBeforeActive); 489 | tabInlineStyles.tabAfterActive = _styleOverride2.default.merge(_TabStyles2.default.tabAfterActive, this.props.tabsStyles.tabAfterActive); 490 | 491 | tabInlineStyles.tabOnHover = _styleOverride2.default.merge(_TabStyles2.default.tabOnHover, this.props.tabsStyles.tabOnHover); 492 | tabInlineStyles.tabTitleOnHover = _styleOverride2.default.merge(_TabStyles2.default.tabTitleOnHover, this.props.tabsStyles.tabTitleOnHover); 493 | tabInlineStyles.tabBeforeOnHover = _styleOverride2.default.merge(_TabStyles2.default.tabBeforeOnHover, this.props.tabsStyles.tabBeforeOnHover); 494 | tabInlineStyles.tabAfterOnHover = _styleOverride2.default.merge(_TabStyles2.default.tabAfterOnHover, this.props.tabsStyles.tabAfterOnHover); 495 | 496 | // append tabs classNames 497 | var xtabClassNames = {}; 498 | xtabClassNames.tabWrapper = (0, _classnames2.default)('rdTabWrapper', this.props.tabsClassNames.tabWrapper); 499 | xtabClassNames.tabBar = (0, _classnames2.default)('rdTabBar', this.props.tabsClassNames.tabBar); 500 | xtabClassNames.tabBarAfter = (0, _classnames2.default)('rdTabBarAfter', this.props.tabsClassNames.tabBarAfter); 501 | xtabClassNames.tab = (0, _classnames2.default)('rdTab', this.props.tabsClassNames.tab); 502 | xtabClassNames.tabBefore = (0, _classnames2.default)('rdTabBefore', this.props.tabsClassNames.tabBefore); 503 | xtabClassNames.tabAfter = (0, _classnames2.default)('rdTabAfter', this.props.tabsClassNames.tabAfter); 504 | xtabClassNames.tabTitle = (0, _classnames2.default)('rdTabTitle', this.props.tabsClassNames.tabTitle); 505 | xtabClassNames.tabBeforeTitle = (0, _classnames2.default)('rdTabBeforeTitle', this.props.tabsClassNames.tabBeforeTitle); 506 | xtabClassNames.tabAfterTitle = (0, _classnames2.default)('rdTabAfterTitle', this.props.tabsClassNames.tabAfterTitle); 507 | xtabClassNames.tabCloseIcon = (0, _classnames2.default)('rdTabCloseIcon', this.props.tabsClassNames.tabCloseIcon); 508 | 509 | var content = []; 510 | var tabs = _lodash2.default.map(this.state.tabs, function (tab) { 511 | if (_this11.state.closedTabs.indexOf(tab.key) > -1) { 512 | return ''; 513 | } 514 | 515 | var _tab$props = tab.props, 516 | beforeTitle = _tab$props.beforeTitle, 517 | title = _tab$props.title, 518 | afterTitle = _tab$props.afterTitle, 519 | tabClassNames = _tab$props.tabClassNames, 520 | tabStyles = _tab$props.tabStyles, 521 | containerStyle = _tab$props.containerStyle, 522 | hiddenContainerStyle = _tab$props.hiddenContainerStyle, 523 | onMouseEnter = _tab$props.onMouseEnter, 524 | onMouseLeave = _tab$props.onMouseLeave, 525 | unclosable = _tab$props.unclosable, 526 | others = _objectWithoutProperties(_tab$props, ['beforeTitle', 'title', 'afterTitle', 'tabClassNames', 'tabStyles', 'containerStyle', 'hiddenContainerStyle', 'onMouseEnter', 'onMouseLeave', 'unclosable']); 527 | 528 | // override inline each tab styles 529 | 530 | 531 | var tabStyle = _styleOverride2.default.merge(tabInlineStyles.tab, tabStyles.tab); 532 | var tabBeforeStyle = _styleOverride2.default.merge(tabInlineStyles.tabBefore, tabStyles.tabBefore); 533 | var tabAfterStyle = _styleOverride2.default.merge(tabInlineStyles.tabAfter, tabStyles.tabAfter); 534 | var tabTiteleStyle = _styleOverride2.default.merge(tabInlineStyles.tabTitle, tabStyles.tabTitle); 535 | var tabCloseIconStyle = _styleOverride2.default.merge(tabInlineStyles.tabCloseIcon, tabStyles.tabCloseIcon); 536 | 537 | var tabClasses = (0, _classnames2.default)(xtabClassNames.tab, tabClassNames.tab); 538 | var tabBeforeClasses = (0, _classnames2.default)(xtabClassNames.tabBefore, tabClassNames.tabBefore); 539 | var tabAfterClasses = (0, _classnames2.default)(xtabClassNames.tabAfter, tabClassNames.tabAfter); 540 | var tabTitleClasses = (0, _classnames2.default)(xtabClassNames.tabTitle, tabClassNames.tabTitle); 541 | var tabBeforeTitleClasses = (0, _classnames2.default)(xtabClassNames.tabBeforeTitle, tabClassNames.tabBeforeTitle); 542 | var tabAfterTitleClasses = (0, _classnames2.default)(xtabClassNames.tabAfterTitle, tabClassNames.tabAfterTitle); 543 | var tabCloseIconClasses = (0, _classnames2.default)(xtabClassNames.tabCloseIcon, tabClassNames.tabCloseIcon); 544 | 545 | if (_this11.state.selectedTab === tab.key) { 546 | tabStyle = _styleOverride2.default.merge(_styleOverride2.default.merge(tabInlineStyles.tab, tabInlineStyles.tabActive), tabStyles.tabActive); 547 | tabBeforeStyle = _styleOverride2.default.merge(_styleOverride2.default.merge(tabInlineStyles.tabBefore, tabInlineStyles.tabBeforeActive), tabStyles.tabBeforeActive); 548 | tabAfterStyle = _styleOverride2.default.merge(_styleOverride2.default.merge(tabInlineStyles.tabAfter, tabInlineStyles.tabAfterActive), tabStyles.tabAfterActive); 549 | tabTiteleStyle = _styleOverride2.default.merge(_styleOverride2.default.merge(tabInlineStyles.tabTitle, tabInlineStyles.tabTitleActive), tabStyles.tabTitleActive); 550 | tabClasses = (0, _classnames2.default)(tabClasses, 'rdTabActive', _this11.props.tabsClassNames.tabActive, tabClassNames.tabActive); 551 | content.push(_react2.default.createElement( 552 | _TabContainer2.default, 553 | { key: 'tabContainer#' + tab.key, selected: true, 554 | style: containerStyle }, 555 | tab 556 | )); 557 | } else { 558 | if (_this11.state.hoveredTab === tab.key) { 559 | tabStyle = _styleOverride2.default.merge(_styleOverride2.default.merge(tabStyle, tabInlineStyles.tabOnHover), tabStyles.tabOnHover); 560 | tabBeforeStyle = _styleOverride2.default.merge(_styleOverride2.default.merge(tabBeforeStyle, tabInlineStyles.tabBeforeOnHover), tabStyles.tabBeforeOnHover); 561 | tabAfterStyle = _styleOverride2.default.merge(_styleOverride2.default.merge(tabAfterStyle, tabInlineStyles.tabAfterOnHover), tabStyles.tabAfterOnHover); 562 | tabTiteleStyle = _styleOverride2.default.merge(_styleOverride2.default.merge(tabTiteleStyle, tabInlineStyles.tabTitleOnHover), tabStyles.tabTitleOnHover); 563 | tabClasses = (0, _classnames2.default)(tabClasses, 'rdTabHover', _this11.props.tabsClassNames.tabHover, tabClassNames.tabHover); 564 | } 565 | content.push(_react2.default.createElement( 566 | _TabContainer2.default, 567 | { 568 | key: 'tabContainer#' + tab.key, 569 | selected: false, 570 | style: containerStyle, 571 | hiddenStyle: hiddenContainerStyle }, 572 | tab 573 | )); 574 | } 575 | 576 | // title will be shorten with inline style 577 | // { 578 | // overflow: 'hidden', 579 | // whiteSpace: 'nowrap', 580 | // textOverflow: 'ellipsis' 581 | // } 582 | var extraAttribute = {}; 583 | if (typeof title === 'string') { 584 | extraAttribute.title = title; 585 | } 586 | var closeButton = _this11.getCloseButton(tab, tabCloseIconStyle, tabCloseIconClasses, tabInlineStyles.tabCloseIconOnHover); 587 | 588 | return _react2.default.createElement( 589 | _CustomDraggable2.default, 590 | { 591 | key: tab.key, 592 | disabled: _this11.props.disableDrag, 593 | axis: 'x', 594 | cancel: '.rdTabCloseIcon', 595 | start: { x: 0, y: 0 }, 596 | moveOnStartChange: true, 597 | zIndex: 100, 598 | bounds: 'parent', 599 | onStart: _this11.handleDragStart.bind(_this11, tab.key), 600 | onDrag: _this11.handleDrag.bind(_this11, tab.key), 601 | onStop: _this11.handleDragStop.bind(_this11, tab.key) }, 602 | _react2.default.createElement( 603 | 'li', 604 | _extends({ style: tabStyle, className: tabClasses, 605 | onClick: _this11.handleTabClick.bind(_this11, tab.key), 606 | onMouseEnter: _this11.handleMouseEnter.bind(_this11, tab.key, onMouseEnter), 607 | onMouseLeave: _this11.handleMouseLeave.bind(_this11, tab.key, onMouseLeave), 608 | ref: tab.key 609 | }, others), 610 | _react2.default.createElement( 611 | 'span', 612 | { style: _TabStyles2.default.beforeTitle, className: tabBeforeTitleClasses }, 613 | beforeTitle 614 | ), 615 | _react2.default.createElement( 616 | 'p', 617 | _extends({ style: tabTiteleStyle, 618 | className: tabTitleClasses 619 | }, extraAttribute), 620 | title 621 | ), 622 | _react2.default.createElement( 623 | 'span', 624 | { style: _TabStyles2.default.afterTitle, className: tabAfterTitleClasses }, 625 | afterTitle 626 | ), 627 | closeButton, 628 | _react2.default.createElement('span', { style: tabBeforeStyle, className: tabBeforeClasses }), 629 | _react2.default.createElement('span', { style: tabAfterStyle, className: tabAfterClasses }) 630 | ) 631 | ); 632 | }); 633 | 634 | return _react2.default.createElement( 635 | 'div', 636 | { style: tabInlineStyles.tabWrapper, className: xtabClassNames.tabWrapper }, 637 | _react2.default.createElement( 638 | 'ul', 639 | { tabIndex: '-1', style: tabInlineStyles.tabBar, className: xtabClassNames.tabBar }, 640 | tabs, 641 | _react2.default.createElement( 642 | 'li', 643 | { className: 'rdTabAddButton', style: _TabStyles2.default.tabAddButton, onClick: this.handleAddButtonClick.bind(this) }, 644 | this.props.tabAddButton 645 | ) 646 | ), 647 | _react2.default.createElement('span', { style: tabInlineStyles.tabBarAfter, className: xtabClassNames.tabBarAfter }), 648 | content 649 | ); 650 | } 651 | }], [{ 652 | key: 'unbindShortcuts', 653 | value: function unbindShortcuts() { 654 | _mousetrap2.default.reset(); 655 | } 656 | }]); 657 | 658 | return Tabs; 659 | }(_react2.default.Component); 660 | 661 | Tabs.defaultProps = { 662 | tabsClassNames: { 663 | tabWrapper: '', 664 | tabBar: '', 665 | tabBarAfter: '', 666 | tab: '', 667 | tabBefore: '', 668 | tabAfter: '', 669 | tabBeforeTitle: '', 670 | tabTitle: '', 671 | tabAfterTitle: '', 672 | tabCloseIcon: '', 673 | tabActive: '', 674 | tabHover: '' 675 | }, 676 | tabsStyles: {}, 677 | shortCutKeys: {}, 678 | tabAddButton: _react2.default.createElement( 679 | 'span', 680 | null, 681 | '+' 682 | ), 683 | onTabSelect: function onTabSelect() {}, 684 | onTabClose: function onTabClose() {}, 685 | onTabAddButtonClick: function onTabAddButtonClick() {}, 686 | onTabPositionChange: function onTabPositionChange() {}, 687 | shouldTabClose: function shouldTabClose() { 688 | return true; 689 | }, 690 | keepSelectedTab: false, 691 | disableDrag: false 692 | }; 693 | 694 | Tabs.propTypes = { 695 | tabs: _propTypes2.default.arrayOf(_propTypes2.default.element), 696 | 697 | selectedTab: _propTypes2.default.string, 698 | tabsClassNames: _propTypes2.default.shape({ 699 | tabWrapper: _propTypes2.default.string, 700 | tabBar: _propTypes2.default.string, 701 | tabBarAfter: _propTypes2.default.string, 702 | tab: _propTypes2.default.string, 703 | tabBefore: _propTypes2.default.string, 704 | tabAfter: _propTypes2.default.string, 705 | tabBeforeTitle: _propTypes2.default.string, 706 | tabTitle: _propTypes2.default.string, 707 | tabAfterTitle: _propTypes2.default.string, 708 | tabCloseIcon: _propTypes2.default.string, 709 | tabActive: _propTypes2.default.string, 710 | tabHover: _propTypes2.default.string 711 | }), 712 | tabsStyles: _propTypes2.default.shape({ 713 | tabWrapper: _propTypes2.default.object, 714 | tabBar: _propTypes2.default.object, 715 | tabBarAfter: _propTypes2.default.object, 716 | tab: _propTypes2.default.object, 717 | tabBefore: _propTypes2.default.object, 718 | tabAfter: _propTypes2.default.object, 719 | tabTitle: _propTypes2.default.object, 720 | tabActive: _propTypes2.default.object, 721 | tabTitleActive: _propTypes2.default.object, 722 | tabBeforeActive: _propTypes2.default.object, 723 | tabAfterActive: _propTypes2.default.object, 724 | tabOnHover: _propTypes2.default.object, 725 | tabTitleOnHover: _propTypes2.default.object, 726 | tabBeforeOnHover: _propTypes2.default.object, 727 | tabAfterOnHover: _propTypes2.default.object, 728 | tabCloseIcon: _propTypes2.default.object, 729 | tabCloseIconOnHover: _propTypes2.default.object 730 | }), 731 | shortCutKeys: _propTypes2.default.shape({ 732 | close: _propTypes2.default.oneOfType([_propTypes2.default.string, _propTypes2.default.arrayOf(_propTypes2.default.string)]), 733 | create: _propTypes2.default.oneOfType([_propTypes2.default.string, _propTypes2.default.arrayOf(_propTypes2.default.string)]), 734 | moveRight: _propTypes2.default.oneOfType([_propTypes2.default.string, _propTypes2.default.arrayOf(_propTypes2.default.string)]), 735 | moveLeft: _propTypes2.default.oneOfType([_propTypes2.default.string, _propTypes2.default.arrayOf(_propTypes2.default.string)]) 736 | }), 737 | tabAddButton: _propTypes2.default.element, 738 | onTabSelect: _propTypes2.default.func, 739 | onTabClose: _propTypes2.default.func, 740 | onTabAddButtonClick: _propTypes2.default.func, 741 | onTabPositionChange: _propTypes2.default.func, 742 | shouldTabClose: _propTypes2.default.func, 743 | keepSelectedTab: _propTypes2.default.bool, 744 | disableDrag: _propTypes2.default.bool 745 | }; 746 | 747 | exports.default = Tabs; -------------------------------------------------------------------------------- /lib/helpers/styleOverride.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _immutable = require('immutable'); 8 | 9 | var _immutable2 = _interopRequireDefault(_immutable); 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 12 | 13 | // eslint-disable-next-line new-cap 14 | var merge = function (original, override) { 15 | return _immutable2.default.Map(original).merge(override).toObject(); 16 | }; 17 | 18 | exports.default = { 19 | merge: merge 20 | }; -------------------------------------------------------------------------------- /lib/helpers/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | var slideArray = function (array, a, b) { 7 | var retArr = void 0; 8 | var xarray = array.slice(0); 9 | 10 | if (a < b) { 11 | retArr = xarray.map(function (v, idx) { 12 | if (idx < a) { 13 | return v; 14 | } else if (a <= idx && idx < b) { 15 | return array[idx + 1]; 16 | } else if (idx === b) { 17 | return array[a]; 18 | } 19 | return v; 20 | }); 21 | } else { 22 | retArr = xarray.map(function (v, idx) { 23 | if (idx < b) { 24 | return v; 25 | } else if (b === idx) { 26 | return array[a]; 27 | } else if (b < idx && idx <= a) { 28 | return array[idx - 1]; 29 | } 30 | return v; 31 | }); 32 | } 33 | return retArr; 34 | }; 35 | 36 | exports.default = { 37 | slideArray: slideArray 38 | }; -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.Tabs = exports.Tab = undefined; 7 | 8 | var _Tab = require('./components/Tab'); 9 | 10 | var _Tab2 = _interopRequireDefault(_Tab); 11 | 12 | var _Tabs = require('./components/Tabs'); 13 | 14 | var _Tabs2 = _interopRequireDefault(_Tabs); 15 | 16 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 17 | 18 | exports.Tab = _Tab2.default; 19 | exports.Tabs = _Tabs2.default; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-draggable-tab", 3 | "version": "0.10.1", 4 | "description": "Draggable chrome like tab react component ", 5 | "main": "./lib/index.js", 6 | "scripts": { 7 | "browser": "browser-sync start --files 'example/bundle.js' 'example/index.html' --server example", 8 | "watch:example": "watchify example/app.js -dv -o example/bundle.js -t [ babelify ]", 9 | "start:example:basex": "npm run clean && npm run build && (npm run watch:example & npm run browser)", 10 | "start:example:base": "npm run clean && npm run build", 11 | "start:example": "cross-env NODE_ENV=development npm run start:example:basex", 12 | "start:example:prod": "cross-env NODE_ENV=production npm run start:example:base", 13 | "test:local": "karma start", 14 | "test": "./node_modules/.bin/karma start --browsers Firefox --single-run", 15 | "clean": "rimraf lib", 16 | "build": "cross-env NODE_ENV=production babel src --out-dir lib" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git://github.com/georgeosddev/react-draggable-tab" 21 | }, 22 | "keywords": [ 23 | "react", 24 | "react-component", 25 | "tab", 26 | "draggable" 27 | ], 28 | "author": "Takeharu.Oshida", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/georgeosddev/react-draggable-tab/issues" 32 | }, 33 | "homepage": "https://github.com/georgeosddev/react-draggable-tab", 34 | "devDependencies": { 35 | "babel-cli": "^6.26.0", 36 | "babel-core": "^6.26.0", 37 | "babel-eslint": "^8.2.1", 38 | "babel-plugin-minify-dead-code-elimination": "^0.2.0", 39 | "babel-preset-env": "^1.6.1", 40 | "babel-preset-es2015": "^6.24.1", 41 | "babel-preset-react": "^6.24.1", 42 | "babel-preset-stage-0": "^6.24.1", 43 | "babel-preset-stage-2": "^6.24.1", 44 | "babelify": "^8.0.0", 45 | "browser-sync": "^2.23.5", 46 | "browserify": "^15.2.0", 47 | "chai": "^4.1.2", 48 | "cross-env": "^5.1.4", 49 | "eslint": "^4.16.0", 50 | "eslint-config-airbnb": "^16.1.0", 51 | "eslint-plugin-import": "^2.8.0", 52 | "eslint-plugin-jsx-a11y": "^6.0.3", 53 | "eslint-plugin-react": "^7.5.1", 54 | "karma": "^2.0.0", 55 | "karma-browserify": "^5.1.3", 56 | "karma-chai": "^0.1.0", 57 | "karma-chrome-launcher": "^2.2.0", 58 | "karma-cli": "^1.0.1", 59 | "karma-firefox-launcher": "^1.1.0", 60 | "karma-mocha": "^1.3.0", 61 | "karma-safari-launcher": "^1.0.0", 62 | "karma-spec-reporter": "^0.0.32", 63 | "material-ui": "^0.20.0", 64 | "mocha": "^5.0.0", 65 | "react-notification-badge": "^1.3.4", 66 | "react-tap-event-plugin": "^3.0.2", 67 | "react-test-renderer": "^16.3.2", 68 | "rimraf": "^2.6.2", 69 | "watchify": "^3.9.0" 70 | }, 71 | "dependencies": { 72 | "classnames": "^2.2.5", 73 | "immutable": "^3.8.2", 74 | "invariant": "^2.2.2", 75 | "lodash": "^4.17.4", 76 | "mousetrap": "^1.6.1", 77 | "prop-types": "^15.6.0", 78 | "react": "^16.2.0", 79 | "react-dom": "^16.2.0", 80 | "react-draggable": "^3.0.5" 81 | }, 82 | "peerDependencies": {}, 83 | "babelify": { 84 | "transform": [ 85 | [ 86 | "babel", 87 | { 88 | "compact": false, 89 | "presets": [ 90 | "env", 91 | "react", 92 | "stage-0" 93 | ], 94 | "env": { 95 | "production": { 96 | "plugins": [ 97 | "minify-dead-code-elimination" 98 | ] 99 | } 100 | } 101 | } 102 | ] 103 | ] 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/components/CloseIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classNames from 'classnames'; 4 | import TabStyles from './TabStyles'; 5 | import StyleOverride from '../helpers/styleOverride'; 6 | 7 | class CloseIcon extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | hover: false, 12 | }; 13 | } 14 | 15 | handleMouseEnter() { 16 | this.setState({ hover: true }); 17 | } 18 | 19 | handleMouseLeave() { 20 | this.setState({ hover: false }); 21 | } 22 | 23 | handleClick(e) { 24 | this.props.onClick(e); 25 | } 26 | 27 | render() { 28 | let iconStyle = this.props.style; 29 | let { className } = this.props; 30 | if (this.state.hover) { 31 | iconStyle = StyleOverride.merge( 32 | this.props.style, 33 | StyleOverride.merge(TabStyles.tabCloseIconOnHover, this.props.hoverStyle), 34 | ); 35 | className = classNames(this.props.className, 'hover'); 36 | } else { 37 | iconStyle = this.props.style; 38 | } 39 | 40 | return ( 41 | 47 | {this.props.children} 48 | 49 | ); 50 | } 51 | } 52 | 53 | CloseIcon.defaultProps = { 54 | style: {}, 55 | hoverStyle: {}, 56 | onClick: () => { 57 | }, 58 | }; 59 | 60 | CloseIcon.propTypes = { 61 | style: PropTypes.object, 62 | hoverStyle: PropTypes.object, 63 | onClick: PropTypes.func, 64 | }; 65 | 66 | export default CloseIcon; 67 | -------------------------------------------------------------------------------- /src/components/CustomDraggable.js: -------------------------------------------------------------------------------- 1 | import Draggable from 'react-draggable'; 2 | 3 | class CustomDraggable extends Draggable { 4 | componentWillReceiveProps(nextProps) { 5 | const newState = { 6 | clientX: nextProps.start.x, 7 | clientY: nextProps.start.y, 8 | }; 9 | this.setState(newState); 10 | } 11 | } 12 | 13 | export default CustomDraggable; 14 | -------------------------------------------------------------------------------- /src/components/Tab.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import PropTypes from 'prop-types'; 4 | 5 | class Tab extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | }; 10 | } 11 | 12 | render() { 13 | return this.props.children; 14 | } 15 | } 16 | 17 | Tab.defaultProps = { 18 | beforeTitle: , 19 | title: 'untitled', 20 | afterTitle: , 21 | unclosable: false, 22 | tabClassNames: { 23 | tab: '', 24 | tabBefore: '', 25 | tabAfter: '', 26 | tabTitle: '', 27 | tabBeforeTitle: '', 28 | tabAfterTitle: '', 29 | tabCloseIcon: '', 30 | tabActive: '', 31 | tabHover: '', 32 | }, 33 | tabStyles: {}, 34 | containerStyle: {}, 35 | }; 36 | 37 | Tab.propTypes = { 38 | beforeTitle: PropTypes.element, 39 | title: PropTypes.oneOfType([ 40 | PropTypes.string, 41 | PropTypes.element, 42 | ]).isRequired, 43 | afterTitle: PropTypes.element, 44 | unclosable: PropTypes.bool, 45 | tabClassNames: PropTypes.shape({ 46 | tab: PropTypes.string, 47 | tabBefore: PropTypes.string, 48 | tabAfter: PropTypes.string, 49 | tabBeforeTitle: PropTypes.string, 50 | tabTitle: PropTypes.string, 51 | tabAfterTitle: PropTypes.string, 52 | tabCloseIcon: PropTypes.string, 53 | tabActive: PropTypes.string, 54 | tabHover: PropTypes.string, 55 | }), 56 | tabStyles: PropTypes.shape({ 57 | tab: PropTypes.object, 58 | tabBefore: PropTypes.object, 59 | tabAfter: PropTypes.object, 60 | tabTitle: PropTypes.object, 61 | tabActive: PropTypes.object, 62 | tabTitleActive: PropTypes.object, 63 | tabBeforeActive: PropTypes.object, 64 | tabAfterActive: PropTypes.object, 65 | tabOnHover: PropTypes.object, 66 | tabTitleOnHover: PropTypes.object, 67 | tabBeforeOnHover: PropTypes.object, 68 | tabAfterOnHover: PropTypes.object, 69 | tabCloseIcon: PropTypes.object, 70 | tabCloseIconHover: PropTypes.object, 71 | }), 72 | containerStyle: PropTypes.object, 73 | hiddenContainerStyle: PropTypes.object, 74 | }; 75 | 76 | export default Tab; 77 | -------------------------------------------------------------------------------- /src/components/TabContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import StyleOverride from '../helpers/styleOverride'; 4 | 5 | const styles = { 6 | root: { 7 | width: '100%', 8 | position: 'relative', 9 | textAlign: 'initial', 10 | }, 11 | }; 12 | 13 | class TabContainer extends React.Component { 14 | render() { 15 | let style = StyleOverride.merge(styles.root, this.props.style); 16 | if (!this.props.selected) { 17 | style = StyleOverride.merge(styles.root, this.props.hiddenStyle); 18 | } 19 | return ( 20 |
21 | {this.props.children} 22 |
23 | ); 24 | } 25 | } 26 | 27 | TabContainer.defaultProps = { 28 | selected: false, 29 | style: {}, 30 | hiddenStyle: { 31 | height: '0px', 32 | overflow: 'hidden', 33 | }, 34 | }; 35 | 36 | TabContainer.propTypes = { 37 | selected: PropTypes.bool.isRequired, 38 | style: PropTypes.object, 39 | hiddenStyle: PropTypes.object, 40 | }; 41 | 42 | export default TabContainer; 43 | -------------------------------------------------------------------------------- /src/components/TabStyles.js: -------------------------------------------------------------------------------- 1 | /* Inspired from Atom 2 | https://github.com/atom/tabs 3 | https://github.com/atom/atom-dark-ui 4 | */ 5 | const TabStyles = { 6 | 7 | tabWrapper: { 8 | height: '100%', 9 | width: '100%', 10 | position: 'relative', 11 | }, 12 | 13 | tabBar: { 14 | // @TODO safari needs prefix. Style should be define in CSS. 15 | // Can't use duprecated key's for inline-style. 16 | // See https://github.com/facebook/react/issues/2020 17 | // display: '-webkit-flex', 18 | // display: '-ms-flexbox', 19 | display: 'flex', 20 | WebkitUserSelect: 'none', 21 | MozUserSelect: 'none', 22 | msUserSelect: 'none', 23 | userSelect: 'none', 24 | margin: 0, 25 | listStyle: 'none', 26 | outline: '0px', 27 | overflowY: 'hidden', 28 | overflowX: 'hidden', 29 | minWidth: '95%', 30 | maxWidth: '99%', 31 | paddingRight: '35px', 32 | }, 33 | 34 | tabBarAfter: { 35 | content: '', 36 | position: 'absolute', 37 | top: '26px', 38 | height: '5px', 39 | left: 0, 40 | right: 0, 41 | zIndex: 2, 42 | backgroundColor: '#222222', 43 | borderBottom: '1px solid #111111', 44 | pointerEvents: 'none', 45 | }, 46 | 47 | tab: { 48 | fontFamily: "'Lucida Grande', 'Segoe UI', Ubuntu, Cantarell, sans-serif", 49 | backgroundImage: 'linear-gradient(#454545, #333333)', 50 | height: '26px', 51 | fontSize: '11px', 52 | position: 'relative', 53 | marginLeft: '30px', 54 | paddingLeft: '15px', 55 | paddingRight: '24px', 56 | WebkutBoxFlex: 1, 57 | WebkitFlex: 1, 58 | MozFlex: 1, 59 | msFlex: 1, 60 | flex: 1, 61 | maxWidth: '175px', 62 | minWidth: '0px', 63 | transform: 'translate(0px, 0px)', 64 | }, 65 | 66 | tabBefore: { 67 | content: '', 68 | position: 'absolute', 69 | top: '0px', 70 | width: '25px', 71 | height: '26px', 72 | 73 | left: '-14px', 74 | borderTopLeftRadius: '3px', 75 | boxShadow: 'inset 1px 1px 0 #484848, -4px 0px 4px rgba(0, 0, 0, 0.1)', 76 | WebkitTransform: 'skewX(-30deg)', 77 | MozTransform: 'skewX(-30deg)', 78 | msTransform: 'skewX(-30deg)', 79 | transform: 'skewX(-30deg)', 80 | backgroundImage: 'linear-gradient(#454545, #333333)', 81 | borderRadius: '7.5px 0 0 0', 82 | }, 83 | 84 | tabAfter: { 85 | content: '', 86 | position: 'absolute', 87 | top: '0px', 88 | width: '25px', 89 | height: '26px', 90 | 91 | right: '-14px', 92 | borderTopRightRadius: '3px', 93 | boxShadow: 'inset -1px 1px 0 #484848, 4px 0px 4px rgba(0, 0, 0, 0.1)', 94 | WebkitTransform: 'skewX(30deg)', 95 | MozTransform: 'skewX(30deg)', 96 | msTransform: 'skewX(30deg)', 97 | transform: 'skewX(30deg)', 98 | backgroundImage: 'linear-gradient(#454545, #333333)', 99 | borderRadius: '0 7.5px 0 0', 100 | }, 101 | 102 | tabTitle: { 103 | cursor: 'default', 104 | overflow: 'hidden', 105 | whiteSpace: 'nowrap', 106 | textOverflow: 'ellipsis', 107 | marginTop: '8px', 108 | float: 'left', 109 | textAlign: 'center', 110 | postion: 'relative', 111 | width: '90%', 112 | color: 'rgb(170, 170, 170)', 113 | }, 114 | 115 | tabActive: { 116 | WebkutBoxFlex: 2, 117 | WebkitFlex: 2, 118 | MozFlex: 2, 119 | msFlex: 2, 120 | flex: 2, 121 | zIndex: 1, 122 | color: '#ffffff', 123 | fontSize: '13px', 124 | backgroundImage: 'linear-gradient(#343434, #222222)', 125 | }, 126 | 127 | tabBeforeActive: { 128 | backgroundImage: 'linear-gradient(#343434, #222222)', 129 | }, 130 | 131 | tabAfterActive: { 132 | backgroundImage: 'linear-gradient(#343434, #222222)', 133 | }, 134 | 135 | tabTitleActive: { 136 | lineHeight: '1.5em', 137 | color: 'rgb(255, 255, 255)', 138 | marginTop: '6px', 139 | }, 140 | 141 | tabOnHover: { 142 | backgroundImage: 'linear-gradient(#333333, #222222)', 143 | }, 144 | 145 | tabBeforeOnHover: { 146 | backgroundImage: 'linear-gradient(#333333, #222222)', 147 | }, 148 | 149 | tabAfterOnHover: { 150 | backgroundImage: 'linear-gradient(#333333, #222222)', 151 | }, 152 | 153 | tabTitleOnHover: { 154 | filter: 'alpha(opacity=20)', 155 | }, 156 | 157 | tabCloseIcon: { 158 | position: 'absolute', 159 | cursor: 'pointer', 160 | font: '16px arial, sans-serif', 161 | right: '-2px', 162 | marginTop: '8px', 163 | textDecoration: 'none', 164 | textShadow: '0 1px 0 #fff', 165 | lineHeight: '1em', 166 | filter: 'alpha(opacity=20)', 167 | opacity: '.2', 168 | width: '16px', 169 | height: '16px', 170 | textAlign: 'center', 171 | WebkitBorderRadius: '8px', 172 | MozBorderRadius: '8px', 173 | borderRadius: '8px', 174 | zIndex: 999, 175 | }, 176 | 177 | tabCloseIconOnHover: { 178 | filter: 'none', 179 | backgroundColor: 'red', 180 | color: 'white', 181 | }, 182 | 183 | tabAddButton: { 184 | cursor: 'pointer', 185 | fontFamily: "'Lucida Grande', 'Segoe UI', Ubuntu, Cantarell, sans-serif", 186 | fontSize: '20px', 187 | textShadow: 'rgb(255, 255, 255) 0px 1px 0px', 188 | position: 'relative', 189 | width: '25px', 190 | height: '26px', 191 | marginLeft: '20px', 192 | zIndex: 2, 193 | }, 194 | 195 | beforeTitle: { 196 | position: 'absolute', 197 | top: '8px', 198 | left: '-8px', 199 | zIndex: 2, 200 | }, 201 | 202 | afterTitle: { 203 | position: 'absolute', 204 | top: '8px', 205 | right: '16px', 206 | zIndex: 2, 207 | }, 208 | }; 209 | 210 | export default TabStyles; 211 | -------------------------------------------------------------------------------- /src/components/Tabs.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import PropTypes from 'prop-types'; 4 | import _ from 'lodash'; 5 | import invariant from 'invariant'; 6 | import classNames from 'classnames'; 7 | import Mousetrap from 'mousetrap'; 8 | 9 | import CustomDraggable from './CustomDraggable'; 10 | import TabStyles from './TabStyles'; 11 | import TabContainer from './TabContainer'; 12 | import CloseIcon from './CloseIcon'; 13 | 14 | import StyleOverride from '../helpers/styleOverride'; 15 | import Utils from '../helpers/utils'; 16 | 17 | function tabStateFromProps(props) { 18 | const tabs = []; 19 | let idx = 0; 20 | React.Children.forEach(props.tabs, (tab) => { 21 | invariant( 22 | tab.key, 23 | 'There should be unique key in each Tab', 24 | ); 25 | 26 | tabs[idx] = tab; 27 | idx += 1; 28 | }); 29 | return { tabs }; 30 | } 31 | 32 | class Tabs extends React.Component { 33 | constructor(props) { 34 | super(props); 35 | 36 | const { tabs } = tabStateFromProps(this.props); 37 | let selectedTab = ''; 38 | if (this.props.selectedTab) { 39 | selectedTab = this.props.selectedTab; // eslint-disable-line prefer-destructuring 40 | } else if (this.props.tabs) { 41 | selectedTab = this.props.tabs[0].key; 42 | } 43 | const hoveredTab = ''; 44 | const closedTabs = []; 45 | this.state = { 46 | tabs, 47 | selectedTab, 48 | hoveredTab, 49 | closedTabs, 50 | }; 51 | 52 | // Dom positons 53 | // do not save in state 54 | this.startPositions = []; 55 | this.dragging = false; 56 | } 57 | 58 | isClosed(key) { 59 | return this.state.closedTabs.indexOf(key) > -1; 60 | } 61 | 62 | getIndexOfTabByKey(key) { 63 | return _.findIndex(this.state.tabs, tab => tab.key === key); 64 | } 65 | 66 | getNextTabKey(key) { 67 | let nextKey; 68 | const current = this.getIndexOfTabByKey(key); 69 | if (current + 1 < this.state.tabs.length) { 70 | nextKey = this.state.tabs[current + 1].key; 71 | if (this.isClosed(nextKey)) { 72 | nextKey = this.getNextTabKey(nextKey); 73 | } 74 | } 75 | return nextKey; 76 | } 77 | 78 | getPrevTabKey(key) { 79 | let prevKey; 80 | const current = this.getIndexOfTabByKey(key); 81 | if (current > 0) { 82 | prevKey = this.state.tabs[current - 1].key; 83 | if (this.isClosed(prevKey)) { 84 | prevKey = this.getPrevTabKey(prevKey); 85 | } 86 | } 87 | return prevKey; 88 | } 89 | 90 | getCurrentOpenTabs() { 91 | return this.getOpenTabs(this.state.tabs); 92 | } 93 | 94 | getOpenTabs(tabs) { 95 | return _.filter(tabs, tab => !this.isClosed(tab.key)); 96 | } 97 | 98 | moveTabPosition(key1, key2) { 99 | const t1 = this.getIndexOfTabByKey(key1); 100 | const t2 = this.getIndexOfTabByKey(key2); 101 | return Utils.slideArray(this.state.tabs, t1, t2); 102 | } 103 | 104 | saveStartPositions() { 105 | // Do not save in state 106 | this.startPositions = _.map(this.state.tabs, (tab) => { 107 | const el = ReactDom.findDOMNode(this.refs[tab.key]); 108 | const pos = el ? el.getBoundingClientRect() : {}; 109 | return { 110 | key: tab.key, 111 | pos, 112 | }; 113 | }); 114 | } 115 | 116 | // eslint-disable-next-line class-methods-use-this 117 | cancelEventSafety(e) { 118 | const ev = e; 119 | if (typeof e.preventDefault !== 'function') { 120 | ev.preventDefault = () => { 121 | }; 122 | } 123 | if (typeof e.stopPropagation !== 'function') { 124 | ev.stopPropagation = () => { 125 | }; 126 | } 127 | ev.preventDefault(); 128 | ev.stopPropagation(); 129 | return ev; 130 | } 131 | 132 | // eslint-disable-next-line class-methods-use-this 133 | componentWillMount() { 134 | } 135 | 136 | componentDidMount() { 137 | this.saveStartPositions(); 138 | this.bindShortcuts(); 139 | } 140 | 141 | componentWillUnmount() { 142 | this.constructor.unbindShortcuts(); 143 | } 144 | 145 | componentWillReceiveProps(nextProps) { 146 | const newState = tabStateFromProps(nextProps); 147 | if (nextProps.selectedTab !== 'undefined') { 148 | newState.selectedTab = nextProps.selectedTab; 149 | } 150 | // reset closedTabs, respect props from application 151 | newState.closedTabs = []; 152 | this.setState(newState); 153 | } 154 | 155 | // eslint-disable-next-line class-methods-use-this 156 | componentWillUpdate() { 157 | } 158 | 159 | componentDidUpdate() { 160 | this.saveStartPositions(); 161 | } 162 | 163 | bindShortcuts() { 164 | if (this.props.shortCutKeys) { 165 | if (this.props.shortCutKeys.close) { 166 | Mousetrap.bind(this.props.shortCutKeys.close, (e) => { 167 | const ev = this.cancelEventSafety(e); 168 | if (this.state.selectedTab) { 169 | this.handleCloseButtonClick(this.state.selectedTab, ev); 170 | } 171 | }); 172 | } 173 | if (this.props.shortCutKeys.create) { 174 | Mousetrap.bind(this.props.shortCutKeys.create, (e) => { 175 | const ev = this.cancelEventSafety(e); 176 | this.handleAddButtonClick(ev); 177 | }); 178 | } 179 | if (this.props.shortCutKeys.moveRight) { 180 | Mousetrap.bind(this.props.shortCutKeys.moveRight, (e) => { 181 | const ev = this.cancelEventSafety(e); 182 | this.moveRight(ev); 183 | }); 184 | } 185 | if (this.props.shortCutKeys.moveLeft) { 186 | Mousetrap.bind(this.props.shortCutKeys.moveLeft, (e) => { 187 | const ev = this.cancelEventSafety(e); 188 | this.moveLeft(ev); 189 | }); 190 | } 191 | } 192 | } 193 | 194 | static unbindShortcuts() { 195 | Mousetrap.reset(); 196 | } 197 | 198 | handleDragStart() { 199 | this.dragging = true; 200 | } 201 | 202 | handleDrag(key, e) { 203 | const deltaX = (e.pageX || e.clientX); 204 | _.each(this.startPositions, (pos) => { 205 | const tempMoved = pos.moved || 0; 206 | const shoudBeSwap = 207 | key !== pos.key && 208 | pos.pos.left + tempMoved < deltaX && 209 | deltaX < pos.pos.right + tempMoved; 210 | if (shoudBeSwap) { 211 | const idx1 = this.getIndexOfTabByKey(key); 212 | const idx2 = this.getIndexOfTabByKey(pos.key); 213 | const minus = idx1 > idx2 ? 1 : -1; 214 | const movePx = (minus * (pos.pos.right - pos.pos.left)) - tempMoved; 215 | ReactDom.findDOMNode(this.refs[pos.key]).style.transform = `translate(${movePx}px, 0px)`; 216 | this.startPositions[idx2].moved = movePx; 217 | } 218 | }); 219 | } 220 | 221 | handleDragStop(key, e) { 222 | const deltaX = (e.pageX || e.clientX); 223 | let swapedTabs = null; 224 | _.each(this.startPositions, (pos) => { 225 | const shoudBeSwap = 226 | key !== pos.key && 227 | pos.pos.left < deltaX && 228 | deltaX < pos.pos.right; 229 | if (shoudBeSwap) { 230 | swapedTabs = this.moveTabPosition(key, pos.key); 231 | } 232 | ReactDom.findDOMNode(this.refs[pos.key]).style.transform = 'translate(0px, 0px)'; 233 | }); 234 | const nextTabs = swapedTabs || this.state.tabs; 235 | 236 | const newState = { 237 | tabs: nextTabs, 238 | selectedTab: key, 239 | }; 240 | this.dragging = false; 241 | this.setState(newState, () => { 242 | if (swapedTabs) { 243 | this.props.onTabPositionChange(e, key, this.getOpenTabs(nextTabs)); 244 | } 245 | }); 246 | } 247 | 248 | handleTabClick(key, e) { 249 | const isBehindTab = key !== this.state.selectedTab; 250 | const idx = this.getIndexOfTabByKey(key); 251 | const isDragAfter = this.startPositions[idx].moved !== 0; 252 | if (isBehindTab && isDragAfter && this.props.keepSelectedTab) { 253 | e.preventDefault(); 254 | return; 255 | } 256 | 257 | const classes = (e.target.getAttribute('class') || '').split(' '); 258 | if (classes.indexOf('rdTabCloseIcon') > -1) { 259 | this.cancelEventSafety(e); 260 | } else { 261 | this.setState({ selectedTab: key }, () => { 262 | this.props.onTabSelect(e, key, this.getCurrentOpenTabs()); 263 | }); 264 | } 265 | } 266 | 267 | handleMouseEnter(key, onMouseEnter, e) { 268 | if (!this.dragging) { 269 | this.setState({ 270 | hoveredTab: key, 271 | }, () => { 272 | if (_.isFunction(onMouseEnter)) { 273 | onMouseEnter(e); 274 | } 275 | }); 276 | } 277 | } 278 | 279 | handleMouseLeave(key, onMouseLeave, e) { 280 | if (!this.dragging) { 281 | if (this.state.hoveredTab === key) { 282 | this.setState({ 283 | hoveredTab: '', 284 | }, () => { 285 | if (_.isFunction(onMouseLeave)) { 286 | onMouseLeave(e); 287 | } 288 | }); 289 | } else if (_.isFunction(onMouseLeave)) { 290 | onMouseLeave(e); 291 | } 292 | } 293 | } 294 | 295 | handleCloseButtonClick(key, e) { 296 | const ev = this.cancelEventSafety(e); 297 | const doClose = () => { 298 | let nextSelected; 299 | 300 | if (this.state.selectedTab === key) { 301 | nextSelected = this.getNextTabKey(key); 302 | if (!nextSelected) { 303 | nextSelected = this.getPrevTabKey(key); 304 | } 305 | } else { 306 | nextSelected = this.state.selectedTab; 307 | } 308 | 309 | const shoudBeNotifyTabChange = this.state.selectedTab !== nextSelected; 310 | this.setState({ 311 | closedTabs: this.state.closedTabs.concat([key]), 312 | selectedTab: nextSelected, 313 | }, () => { 314 | const currentOpenTabs = this.getCurrentOpenTabs(); 315 | this.props.onTabClose(ev, key, currentOpenTabs); 316 | if (shoudBeNotifyTabChange) { 317 | this.props.onTabSelect(ev, nextSelected, currentOpenTabs); 318 | } 319 | }); 320 | }; 321 | if (this.props.shouldTabClose(ev, key)) { 322 | doClose(); 323 | } 324 | } 325 | 326 | handleAddButtonClick(e) { 327 | this.props.onTabAddButtonClick(e, this.getCurrentOpenTabs()); 328 | } 329 | 330 | moveRight(e) { 331 | let nextSelected = this.getNextTabKey(this.state.selectedTab); 332 | if (!nextSelected) { 333 | nextSelected = this.props.tabs[0] ? this.props.tabs[0].key : ''; 334 | } 335 | if (nextSelected !== this.state.selectedTab) { 336 | this.setState({ selectedTab: nextSelected }, () => { 337 | this.props.onTabSelect(e, nextSelected, this.getCurrentOpenTabs()); 338 | }); 339 | } 340 | } 341 | 342 | moveLeft(e) { 343 | let nextSelected = this.getPrevTabKey(this.state.selectedTab); 344 | if (!nextSelected) { 345 | nextSelected = _.last(this.props.tabs) ? _.last(this.props.tabs).key : ''; 346 | } 347 | if (nextSelected !== this.state.selectedTab) { 348 | this.setState({ selectedTab: nextSelected }, () => { 349 | this.props.onTabSelect(e, nextSelected, this.getCurrentOpenTabs()); 350 | }); 351 | } 352 | } 353 | 354 | getCloseButton(tab, style, classes, hoverStyleBase) { 355 | if (tab.props.unclosable) { 356 | return ''; 357 | } 358 | const onHoverStyle = StyleOverride.merge(hoverStyleBase, tab.props.tabStyles.tabCloseIconOnHover); 359 | return (×); 364 | } 365 | 366 | render() { 367 | // override inline tabs styles 368 | const tabInlineStyles = {}; 369 | tabInlineStyles.tabWrapper = StyleOverride.merge(TabStyles.tabWrapper, this.props.tabsStyles.tabWrapper); 370 | tabInlineStyles.tabBar = StyleOverride.merge(TabStyles.tabBar, this.props.tabsStyles.tabBar); 371 | tabInlineStyles.tabBarAfter = StyleOverride.merge(TabStyles.tabBarAfter, this.props.tabsStyles.tabBarAfter); 372 | tabInlineStyles.tab = StyleOverride.merge(TabStyles.tab, this.props.tabsStyles.tab); 373 | tabInlineStyles.tabBefore = StyleOverride.merge(TabStyles.tabBefore, this.props.tabsStyles.tabBefore); 374 | tabInlineStyles.tabAfter = StyleOverride.merge(TabStyles.tabAfter, this.props.tabsStyles.tabAfter); 375 | tabInlineStyles.tabTitle = StyleOverride.merge(TabStyles.tabTitle, this.props.tabsStyles.tabTitle); 376 | tabInlineStyles.tabCloseIcon = StyleOverride.merge(TabStyles.tabCloseIcon, this.props.tabsStyles.tabCloseIcon); 377 | tabInlineStyles.tabCloseIconOnHover = StyleOverride.merge(TabStyles.tabCloseIconOnHover, this.props.tabsStyles.tabCloseIconOnHover); 378 | 379 | tabInlineStyles.tabActive = StyleOverride.merge(TabStyles.tabActive, this.props.tabsStyles.tabActive); 380 | tabInlineStyles.tabTitleActive = StyleOverride.merge(TabStyles.tabTitleActive, this.props.tabsStyles.tabTitleActive); 381 | tabInlineStyles.tabBeforeActive = StyleOverride.merge(TabStyles.tabBeforeActive, this.props.tabsStyles.tabBeforeActive); 382 | tabInlineStyles.tabAfterActive = StyleOverride.merge(TabStyles.tabAfterActive, this.props.tabsStyles.tabAfterActive); 383 | 384 | tabInlineStyles.tabOnHover = StyleOverride.merge(TabStyles.tabOnHover, this.props.tabsStyles.tabOnHover); 385 | tabInlineStyles.tabTitleOnHover = StyleOverride.merge(TabStyles.tabTitleOnHover, this.props.tabsStyles.tabTitleOnHover); 386 | tabInlineStyles.tabBeforeOnHover = StyleOverride.merge(TabStyles.tabBeforeOnHover, this.props.tabsStyles.tabBeforeOnHover); 387 | tabInlineStyles.tabAfterOnHover = StyleOverride.merge(TabStyles.tabAfterOnHover, this.props.tabsStyles.tabAfterOnHover); 388 | 389 | // append tabs classNames 390 | const xtabClassNames = {}; 391 | xtabClassNames.tabWrapper = classNames('rdTabWrapper', this.props.tabsClassNames.tabWrapper); 392 | xtabClassNames.tabBar = classNames('rdTabBar', this.props.tabsClassNames.tabBar); 393 | xtabClassNames.tabBarAfter = classNames('rdTabBarAfter', this.props.tabsClassNames.tabBarAfter); 394 | xtabClassNames.tab = classNames('rdTab', this.props.tabsClassNames.tab); 395 | xtabClassNames.tabBefore = classNames('rdTabBefore', this.props.tabsClassNames.tabBefore); 396 | xtabClassNames.tabAfter = classNames('rdTabAfter', this.props.tabsClassNames.tabAfter); 397 | xtabClassNames.tabTitle = classNames('rdTabTitle', this.props.tabsClassNames.tabTitle); 398 | xtabClassNames.tabBeforeTitle = classNames('rdTabBeforeTitle', this.props.tabsClassNames.tabBeforeTitle); 399 | xtabClassNames.tabAfterTitle = classNames('rdTabAfterTitle', this.props.tabsClassNames.tabAfterTitle); 400 | xtabClassNames.tabCloseIcon = classNames('rdTabCloseIcon', this.props.tabsClassNames.tabCloseIcon); 401 | 402 | 403 | const content = []; 404 | const tabs = _.map(this.state.tabs, (tab) => { 405 | if (this.state.closedTabs.indexOf(tab.key) > -1) { 406 | return ''; 407 | } 408 | const { 409 | beforeTitle, 410 | title, 411 | afterTitle, 412 | tabClassNames, 413 | tabStyles, 414 | containerStyle, 415 | hiddenContainerStyle, 416 | onMouseEnter, 417 | onMouseLeave, 418 | unclosable, 419 | ...others 420 | } = tab.props; 421 | 422 | // override inline each tab styles 423 | let tabStyle = StyleOverride.merge(tabInlineStyles.tab, tabStyles.tab); 424 | let tabBeforeStyle = StyleOverride.merge(tabInlineStyles.tabBefore, tabStyles.tabBefore); 425 | let tabAfterStyle = StyleOverride.merge(tabInlineStyles.tabAfter, tabStyles.tabAfter); 426 | let tabTiteleStyle = StyleOverride.merge(tabInlineStyles.tabTitle, tabStyles.tabTitle); 427 | const tabCloseIconStyle = StyleOverride.merge(tabInlineStyles.tabCloseIcon, tabStyles.tabCloseIcon); 428 | 429 | let tabClasses = classNames(xtabClassNames.tab, tabClassNames.tab); 430 | const tabBeforeClasses = classNames(xtabClassNames.tabBefore, tabClassNames.tabBefore); 431 | const tabAfterClasses = classNames(xtabClassNames.tabAfter, tabClassNames.tabAfter); 432 | const tabTitleClasses = classNames(xtabClassNames.tabTitle, tabClassNames.tabTitle); 433 | const tabBeforeTitleClasses = classNames(xtabClassNames.tabBeforeTitle, tabClassNames.tabBeforeTitle); 434 | const tabAfterTitleClasses = classNames(xtabClassNames.tabAfterTitle, tabClassNames.tabAfterTitle); 435 | const tabCloseIconClasses = classNames(xtabClassNames.tabCloseIcon, tabClassNames.tabCloseIcon); 436 | 437 | if (this.state.selectedTab === tab.key) { 438 | tabStyle = StyleOverride.merge(StyleOverride.merge(tabInlineStyles.tab, tabInlineStyles.tabActive), tabStyles.tabActive); 439 | tabBeforeStyle = StyleOverride.merge(StyleOverride.merge(tabInlineStyles.tabBefore, tabInlineStyles.tabBeforeActive), tabStyles.tabBeforeActive); 440 | tabAfterStyle = StyleOverride.merge(StyleOverride.merge(tabInlineStyles.tabAfter, tabInlineStyles.tabAfterActive), tabStyles.tabAfterActive); 441 | tabTiteleStyle = StyleOverride.merge(StyleOverride.merge(tabInlineStyles.tabTitle, tabInlineStyles.tabTitleActive), tabStyles.tabTitleActive); 442 | tabClasses = classNames(tabClasses, 'rdTabActive', this.props.tabsClassNames.tabActive, tabClassNames.tabActive); 443 | content.push({tab}); 445 | } else { 446 | if (this.state.hoveredTab === tab.key) { 447 | tabStyle = StyleOverride.merge(StyleOverride.merge(tabStyle, tabInlineStyles.tabOnHover), tabStyles.tabOnHover); 448 | tabBeforeStyle = StyleOverride.merge(StyleOverride.merge(tabBeforeStyle, tabInlineStyles.tabBeforeOnHover), tabStyles.tabBeforeOnHover); 449 | tabAfterStyle = StyleOverride.merge(StyleOverride.merge(tabAfterStyle, tabInlineStyles.tabAfterOnHover), tabStyles.tabAfterOnHover); 450 | tabTiteleStyle = StyleOverride.merge(StyleOverride.merge(tabTiteleStyle, tabInlineStyles.tabTitleOnHover), tabStyles.tabTitleOnHover); 451 | tabClasses = classNames(tabClasses, 'rdTabHover', this.props.tabsClassNames.tabHover, tabClassNames.tabHover); 452 | } 453 | content.push({tab}); 458 | } 459 | 460 | // title will be shorten with inline style 461 | // { 462 | // overflow: 'hidden', 463 | // whiteSpace: 'nowrap', 464 | // textOverflow: 'ellipsis' 465 | // } 466 | const extraAttribute = {}; 467 | if (typeof title === 'string') { 468 | extraAttribute.title = title; 469 | } 470 | const closeButton = this.getCloseButton(tab, tabCloseIconStyle, tabCloseIconClasses, tabInlineStyles.tabCloseIconOnHover); 471 | 472 | return ( 473 | 485 |
  • 491 | {beforeTitle} 492 |

    495 | {title} 496 |

    497 | {afterTitle} 498 | {closeButton} 499 | 500 | 501 |
  • 502 |
    503 | ); 504 | }); 505 | 506 | return ( 507 |
    508 |
      509 | {tabs} 510 |
    • 511 | {this.props.tabAddButton} 512 |
    • 513 |
    514 | 515 | {content} 516 |
    517 | ); 518 | } 519 | } 520 | 521 | Tabs.defaultProps = { 522 | tabsClassNames: { 523 | tabWrapper: '', 524 | tabBar: '', 525 | tabBarAfter: '', 526 | tab: '', 527 | tabBefore: '', 528 | tabAfter: '', 529 | tabBeforeTitle: '', 530 | tabTitle: '', 531 | tabAfterTitle: '', 532 | tabCloseIcon: '', 533 | tabActive: '', 534 | tabHover: '', 535 | }, 536 | tabsStyles: {}, 537 | shortCutKeys: {}, 538 | tabAddButton: ({'+'}), 539 | onTabSelect: () => { 540 | }, 541 | onTabClose: () => { 542 | }, 543 | onTabAddButtonClick: () => { 544 | }, 545 | onTabPositionChange: () => { 546 | }, 547 | shouldTabClose: () => true, 548 | keepSelectedTab: false, 549 | disableDrag: false, 550 | }; 551 | 552 | Tabs.propTypes = { 553 | tabs: PropTypes.arrayOf(PropTypes.element), 554 | 555 | selectedTab: PropTypes.string, 556 | tabsClassNames: PropTypes.shape({ 557 | tabWrapper: PropTypes.string, 558 | tabBar: PropTypes.string, 559 | tabBarAfter: PropTypes.string, 560 | tab: PropTypes.string, 561 | tabBefore: PropTypes.string, 562 | tabAfter: PropTypes.string, 563 | tabBeforeTitle: PropTypes.string, 564 | tabTitle: PropTypes.string, 565 | tabAfterTitle: PropTypes.string, 566 | tabCloseIcon: PropTypes.string, 567 | tabActive: PropTypes.string, 568 | tabHover: PropTypes.string, 569 | }), 570 | tabsStyles: PropTypes.shape({ 571 | tabWrapper: PropTypes.object, 572 | tabBar: PropTypes.object, 573 | tabBarAfter: PropTypes.object, 574 | tab: PropTypes.object, 575 | tabBefore: PropTypes.object, 576 | tabAfter: PropTypes.object, 577 | tabTitle: PropTypes.object, 578 | tabActive: PropTypes.object, 579 | tabTitleActive: PropTypes.object, 580 | tabBeforeActive: PropTypes.object, 581 | tabAfterActive: PropTypes.object, 582 | tabOnHover: PropTypes.object, 583 | tabTitleOnHover: PropTypes.object, 584 | tabBeforeOnHover: PropTypes.object, 585 | tabAfterOnHover: PropTypes.object, 586 | tabCloseIcon: PropTypes.object, 587 | tabCloseIconOnHover: PropTypes.object, 588 | }), 589 | shortCutKeys: PropTypes.shape({ 590 | close: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]), 591 | create: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]), 592 | moveRight: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]), 593 | moveLeft: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]), 594 | }), 595 | tabAddButton: PropTypes.element, 596 | onTabSelect: PropTypes.func, 597 | onTabClose: PropTypes.func, 598 | onTabAddButtonClick: PropTypes.func, 599 | onTabPositionChange: PropTypes.func, 600 | shouldTabClose: PropTypes.func, 601 | keepSelectedTab: PropTypes.bool, 602 | disableDrag: PropTypes.bool, 603 | }; 604 | 605 | export default Tabs; 606 | -------------------------------------------------------------------------------- /src/helpers/styleOverride.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | 3 | // eslint-disable-next-line new-cap 4 | const merge = (original, override) => Immutable.Map(original).merge(override).toObject(); 5 | 6 | export default { 7 | merge, 8 | }; 9 | -------------------------------------------------------------------------------- /src/helpers/utils.js: -------------------------------------------------------------------------------- 1 | const slideArray = (array, a, b) => { 2 | let retArr; 3 | const xarray = array.slice(0); 4 | 5 | if (a < b) { 6 | retArr = xarray.map((v, idx) => { 7 | if (idx < a) { 8 | return v; 9 | } else if (a <= idx && idx < b) { 10 | return array[idx + 1]; 11 | } else if (idx === b) { 12 | return array[a]; 13 | } 14 | return v; 15 | }); 16 | } else { 17 | retArr = xarray.map((v, idx) => { 18 | if (idx < b) { 19 | return v; 20 | } else if (b === idx) { 21 | return array[a]; 22 | } else if (b < idx && idx <= a) { 23 | return array[idx - 1]; 24 | } 25 | return v; 26 | }); 27 | } 28 | return retArr; 29 | }; 30 | 31 | export default { 32 | slideArray, 33 | }; 34 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Tab from './components/Tab'; 2 | import Tabs from './components/Tabs'; 3 | 4 | export { 5 | Tab, 6 | Tabs, 7 | }; 8 | -------------------------------------------------------------------------------- /test/components/CloseIcon_spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import React from 'react'; 3 | import ReactDom from 'react-dom'; 4 | import ReactTestUtils from 'react-dom/test-utils'; 5 | import ShallowRenderer from 'react-test-renderer/shallow'; 6 | import chai from 'chai'; 7 | let expect = chai.expect; 8 | import CloseIcon from '../../src/components/CloseIcon'; 9 | 10 | describe('Test of CloseIcon', () => { 11 | const style = {color:'red'}; 12 | const hoverStyle = {color:'yellow'}; 13 | const className = 'myClass'; 14 | 15 | let component; 16 | 17 | beforeEach(() => { 18 | }); 19 | 20 | it('should have default properties', function () { 21 | component = ReactTestUtils.renderIntoDocument(×); 22 | expect(component.props.hoverStyle).to.be.empty; 23 | expect(typeof component.props.onClick).to.be.equal('function'); 24 | }); 25 | 26 | 27 | it('should pass style and className to renderd child', function () { 28 | component = ×; 29 | let renderer = new ShallowRenderer(); 30 | renderer.render(component); 31 | let output = renderer.getRenderOutput(); 32 | 33 | expect(output.props.className).to.be.equal('myClass'); 34 | expect(output.props.style.color).to.be.equal('red'); 35 | }); 36 | 37 | it('should call onClick when clicked', function () { 38 | let called = false; 39 | component = ReactTestUtils.renderIntoDocument( 40 | × 41 | ); 42 | const span = ReactTestUtils.findRenderedDOMComponentWithTag(component, 'span'); 43 | ReactTestUtils.Simulate.click(ReactDom.findDOMNode(span)); 44 | expect(called).to.be.equal(true); 45 | }); 46 | 47 | describe('handle mouseEnter/mouseLeave', () => { 48 | beforeEach(() => { 49 | component = ReactTestUtils.renderIntoDocument( 50 | × 51 | ); 52 | }); 53 | 54 | it('should update style and className on mouseEnter', function () { 55 | const span = ReactTestUtils.findRenderedDOMComponentWithTag(component, 'span'); 56 | ReactTestUtils.Simulate.mouseEnter(span); 57 | expect(ReactDom.findDOMNode(span).classList.contains('hover')).to.be.equal(true); 58 | expect(ReactDom.findDOMNode(span).style.color).to.be.equal('yellow'); 59 | }); 60 | 61 | it('should update style and className on mouseLeave', function () { 62 | const span = ReactTestUtils.findRenderedDOMComponentWithTag(component, 'span'); 63 | ReactTestUtils.Simulate.mouseLeave(span); 64 | expect(ReactDom.findDOMNode(span).classList.contains('hover')).to.be.equal(false); 65 | expect(ReactDom.findDOMNode(span).style.color).to.be.equal('red'); 66 | }); 67 | }); 68 | 69 | 70 | }); 71 | -------------------------------------------------------------------------------- /test/components/TabContainer_spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import React from 'react'; 3 | import ReactDom from 'react-dom'; 4 | import ReactTestUtils from 'react-dom/test-utils'; 5 | import chai from 'chai'; 6 | let expect = chai.expect; 7 | import TabContainer from '../../src/components/TabContainer'; 8 | 9 | describe('Test of TabContainer', () => { 10 | let component; 11 | 12 | beforeEach(() => { 13 | }); 14 | 15 | it('should have default properties', function () { 16 | component = ReactTestUtils.renderIntoDocument(

    test tab

    ); 17 | expect(component.props.selected).to.be.equal(false); 18 | }); 19 | 20 | it('It will be height 0 when not selected', () => { 21 | component = ReactTestUtils.renderIntoDocument(

    not test tab

    ); 22 | const p = ReactTestUtils.findRenderedDOMComponentWithTag(component, 'p'); 23 | expect(ReactDom.findDOMNode(p).parentNode.style.height).to.be.equal('0px'); 24 | }); 25 | 26 | it('It will have visible height when selected', () => { 27 | component = ReactTestUtils.renderIntoDocument(

    not test tab

    ); 28 | const p = ReactTestUtils.findRenderedDOMComponentWithTag(component, 'p'); 29 | expect(ReactDom.findDOMNode(p).parentNode.style.height).to.be.not.equal('0px'); 30 | }); 31 | 32 | it('It will be height 0 with custom style when not selected', () => { 33 | component = ReactTestUtils.renderIntoDocument(

    not test tab

    ); 34 | const p = ReactTestUtils.findRenderedDOMComponentWithTag(component, 'p'); 35 | expect(ReactDom.findDOMNode(p).parentNode.style.height).to.be.equal('0px'); 36 | expect(ReactDom.findDOMNode(p).parentNode.style.color).to.be.not.equal('red'); 37 | }); 38 | 39 | it('It will have unvisible custom style when not selected', () => { 40 | component = ReactTestUtils.renderIntoDocument(

    not test tab

    ); 41 | const p = ReactTestUtils.findRenderedDOMComponentWithTag(component, 'p'); 42 | expect(ReactDom.findDOMNode(p).parentNode.style.opacity).to.be.equal('0'); 43 | expect(ReactDom.findDOMNode(p).parentNode.style.height).to.be.not.equal('0px'); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/components/Tab_spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import React from 'react'; 3 | import ReactDom from 'react-dom'; 4 | import ReactTestUtils from 'react-dom/test-utils'; 5 | import chai from 'chai'; 6 | let expect = chai.expect; 7 | import Tab from '../../src/components/Tab'; 8 | 9 | describe('Test of Tab', () => { 10 | let component; 11 | 12 | beforeEach(() => { 13 | }); 14 | 15 | it('should have default properties', function () { 16 | component = ReactTestUtils.renderIntoDocument(

    test tab

    ); 17 | expect(component.props.title).to.be.equal('untitled'); 18 | expect(component.props.unclosable).to.be.equal(false); 19 | 20 | expect(component.props.tabClassNames).to.be.an('object'); 21 | expect(component.props.tabClassNames.tab).to.be.equal(''); 22 | expect(component.props.tabClassNames.tabBefore).to.be.equal(''); 23 | expect(component.props.tabClassNames.tabAfter).to.be.equal(''); 24 | expect(component.props.tabClassNames.tabTitle).to.be.equal(''); 25 | expect(component.props.tabClassNames.tabBeforeTitle).to.be.equal(''); 26 | expect(component.props.tabClassNames.tabAfterTitle).to.be.equal(''); 27 | expect(component.props.tabClassNames.tabCloseIcon).to.be.equal(''); 28 | expect(component.props.tabClassNames.tabActive).to.be.equal(''); 29 | expect(component.props.tabClassNames.tabHover).to.be.equal(''); 30 | 31 | expect(component.props.tabStyles).to.be.empty; 32 | expect(component.props.containerStyle).to.be.empty; 33 | 34 | }); 35 | 36 | it('Tab works like a scala case class, it just render it\'s children', () => { 37 | component = ReactTestUtils.renderIntoDocument(

    test tab

    ); 38 | const p = ReactTestUtils.findRenderedDOMComponentWithTag(component, 'p'); 39 | expect(ReactDom.findDOMNode(p).textContent).to.be.equal('test tab'); 40 | }); 41 | 42 | it('String can use as title', () => { 43 | component = ReactTestUtils.renderIntoDocument(

    test tab

    ); 44 | const p = ReactTestUtils.findRenderedDOMComponentWithTag(component, 'p'); 45 | expect(ReactDom.findDOMNode(p).textContent).to.be.equal('test tab'); 46 | }); 47 | 48 | it('Element can use as title', () => { 49 | component = ReactTestUtils.renderIntoDocument(HELLO
    } >

    test tab

    ); 50 | const p = ReactTestUtils.findRenderedDOMComponentWithTag(component, 'p'); 51 | expect(ReactDom.findDOMNode(p).textContent).to.be.equal('test tab'); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /test/components/Tabs_spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import ReactTestUtils from 'react-dom/test-utils'; 4 | import chai from 'chai'; 5 | import Mousetrap from 'mousetrap'; 6 | let expect = chai.expect; 7 | import triggerEvent from '../triggerEvent.js'; 8 | 9 | import Tabs from '../../src/components/Tabs'; 10 | import Tab from '../../src/components/Tab'; 11 | 12 | describe('Test of Tabs', () => { 13 | let component; 14 | 15 | beforeEach(() => { 16 | // component = ReactTestUtils.renderIntoDocument(); 17 | }); 18 | 19 | it('should have default properties', function () { 20 | component = ReactTestUtils.renderIntoDocument(); 21 | 22 | expect(component.props.tabsClassNames).to.be.an('object'); 23 | expect(component.props.tabsClassNames.tabWrapper).to.be.equal(''); 24 | expect(component.props.tabsClassNames.tabBar).to.be.equal(''); 25 | expect(component.props.tabsClassNames.tabBarAfter).to.be.equal(''); 26 | expect(component.props.tabsClassNames.tab).to.be.equal(''); 27 | expect(component.props.tabsClassNames.tabBefore).to.be.equal(''); 28 | expect(component.props.tabsClassNames.tabAfter).to.be.equal(''); 29 | expect(component.props.tabsClassNames.tabBeforeTitle).to.be.equal(''); 30 | expect(component.props.tabsClassNames.tabTitle).to.be.equal(''); 31 | expect(component.props.tabsClassNames.tabAfterTitle).to.be.equal(''); 32 | expect(component.props.tabsClassNames.tabCloseIcon).to.be.equal(''); 33 | expect(component.props.tabsClassNames.tabActive).to.be.equal(''); 34 | expect(component.props.tabsClassNames.tabHover).to.be.equal(''); 35 | expect(component.props.disableDrag).to.be.equal(false); 36 | 37 | expect(component.props.tabsStyles).to.be.empty; 38 | 39 | expect(component.props.tabAddButton.type).to.be.equal('span'); 40 | 41 | expect(component.props.shortCutKeys).to.be.empty; 42 | 43 | expect(typeof component.props.onTabSelect).to.be.equal('function'); 44 | expect(typeof component.props.onTabClose).to.be.equal('function'); 45 | expect(typeof component.props.onTabAddButtonClick).to.be.equal('function'); 46 | expect(typeof component.props.shouldTabClose).to.be.equal('function'); 47 | expect(typeof component.props.onTabPositionChange).to.be.equal('function'); 48 | }); 49 | 50 | 51 | describe('render tabs and selected tab content', function(){ 52 | 53 | const tabs = [ 54 | ( 55 |

    tab1Content

    56 |
    ), 57 | ( 58 |

    tab2Content

    59 |
    ), 60 | ( 61 |

    tab3Content

    62 |
    ) 63 | ]; 64 | 65 | beforeEach(() => { 66 | class Wrapper extends React.Component { 67 | getInitialState() { 68 | return {foo: 1}; 69 | } 70 | update() { 71 | this.setState({foo: 2}); 72 | } 73 | render() { 74 | return
    ; 77 | } 78 | } 79 | component = ReactTestUtils.renderIntoDocument(); 80 | }); 81 | 82 | it('render tab title as list', function(){ 83 | let titles = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTabTitle'); 84 | expect(titles).to.be.length(3); 85 | expect(titles[0].textContent).to.be.equal('tab1'); 86 | expect(titles[1].textContent).to.be.equal('tab2'); 87 | expect(titles[2].textContent).to.be.equal('tab3'); 88 | }); 89 | 90 | it('render tab title as title attribute', function(){ 91 | let titles = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTabTitle'); 92 | expect(titles).to.be.length(3); 93 | expect(titles[0].title).to.be.equal('tab1'); 94 | expect(titles[1].title).to.be.equal('tab2'); 95 | expect(titles[2].title).to.be.equal('tab3'); 96 | }); 97 | 98 | it('render first tab as selected', function(){ 99 | let activeTab = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'rdTabActive'); 100 | let title = activeTab.querySelector('p'); 101 | expect(title.textContent).to.be.equal('tab1'); 102 | }); 103 | 104 | it('render first tab\'s content', function(){ 105 | let content = ReactTestUtils.scryRenderedDOMComponentsWithTag(component, 'h1'); 106 | expect(content).to.be.length(3); 107 | expect(content[0].textContent).to.be.equal('tab1Content'); 108 | expect(content[0].parentNode.style.height).to.be.not.equal('0px'); 109 | expect(content[1].textContent).to.be.equal('tab2Content'); 110 | expect(content[1].parentNode.style.height).to.be.equal('0px'); 111 | expect(content[2].textContent).to.be.equal('tab3Content'); 112 | expect(content[2].parentNode.style.height).to.be.equal('0px'); 113 | }); 114 | }); 115 | 116 | describe('render tab title as element', function(){ 117 | 118 | const tabs = [ 119 | (tab1
    } > 120 |

    tab1Content

    121 | ), 122 | (tab2} > 123 |

    tab2Content

    124 |
    ), 125 | (tab3} > 126 |

    tab3Content

    127 |
    ) 128 | ]; 129 | 130 | beforeEach(() => { 131 | component = ReactTestUtils.renderIntoDocument( 132 | ); 135 | }); 136 | 137 | it('render tab title as list', function(){ 138 | let children = ReactTestUtils.scryRenderedDOMComponentsWithTag(component, 'li'); 139 | expect(children).to.be.length(4); 140 | 141 | let t1 = children[0].querySelector('.rdTabTitle'); 142 | expect(t1.textContent).to.be.equal('tab1'); 143 | 144 | let t2 = children[1].querySelector('.rdTabTitle'); 145 | expect(t2.textContent).to.be.equal('tab2'); 146 | 147 | let t3 = children[2].querySelector('.rdTabTitle'); 148 | expect(t3.textContent).to.be.equal('tab3'); 149 | 150 | expect(children[3].className).to.be.equal('rdTabAddButton'); 151 | }); 152 | 153 | it('will not render tab title as title attribute', function(){ 154 | let children = ReactTestUtils.scryRenderedDOMComponentsWithTag(component, 'li'); 155 | expect(children).to.be.length(4); 156 | 157 | let t1 = children[0].querySelector('.rdTabTitle'); 158 | expect(t1.title).to.be.equal(''); 159 | 160 | let t2 = children[1].querySelector('.rdTabTitle'); 161 | expect(t2.title).to.be.equal(''); 162 | 163 | let t3 = children[2].querySelector('.rdTabTitle'); 164 | expect(t3.title).to.be.equal(''); 165 | }); 166 | }); 167 | 168 | describe('add optional element before/after Title', () => { 169 | 170 | const el1 = (); 171 | const el2 = (); 172 | const el3 = (); 173 | const el4 = (); 174 | 175 | const tabs = [ 176 | ( 177 |

    tab1Content

    178 |
    ), 179 | ( 180 |

    tab2Content

    181 |
    ), 182 | ( 183 |

    tab3Content

    184 |
    ) 185 | ]; 186 | let children; 187 | 188 | beforeEach(() => { 189 | component = ReactTestUtils.renderIntoDocument( 190 | ); 193 | children = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTab'); 194 | }); 195 | 196 | it('will insert custome element before title', () => { 197 | let t1BeforeTitle = children[0].querySelector('.rdTabBeforeTitle'); 198 | let t1BeforeTitleIcon = t1BeforeTitle.querySelector('i'); 199 | expect(t1BeforeTitleIcon.className.indexOf('icon1') > 0).to.be.equal(true); 200 | 201 | let t1AfterTitle = children[0].querySelector('.rdTabAfterTitle'); 202 | let t1AfterTitleIcon = t1AfterTitle.querySelector('i'); 203 | expect(t1AfterTitleIcon).to.be.eql(null); 204 | }); 205 | 206 | it('will insert custome element affter title', () => { 207 | let t2BeforeTitle = children[1].querySelector('.rdTabBeforeTitle'); 208 | let t2BeforeTitleIcon = t2BeforeTitle.querySelector('i'); 209 | expect(t2BeforeTitleIcon).to.be.eql(null); 210 | 211 | let t2AfterTitle = children[1].querySelector('.rdTabAfterTitle'); 212 | let t2AfterTitleIcon = t2AfterTitle.querySelector('i'); 213 | expect(t2AfterTitleIcon.className.indexOf('icon2') > 0).to.be.equal(true); 214 | }); 215 | 216 | it('will insert custome element before/affter title', () => { 217 | let t3BeforeTitle = children[2].querySelector('.rdTabBeforeTitle'); 218 | let t3BeforeTitleIcon = t3BeforeTitle.querySelector('i'); 219 | expect(t3BeforeTitleIcon.className.indexOf('icon3') > 0).to.be.equal(true); 220 | 221 | let t3AfterTitle = children[2].querySelector('.rdTabAfterTitle'); 222 | let t3AfterTitleIcon = t3AfterTitle.querySelector('i'); 223 | expect(t3AfterTitleIcon.className.indexOf('icon4') > 0).to.be.equal(true); 224 | }); 225 | }); 226 | 227 | describe('add custom className to all tabs ', function(){ 228 | 229 | const tabsClassNames = { 230 | tabWrapper: 'myWrapper', 231 | tabBar: 'myTabBar', 232 | tabBarAfter: 'myTabBarAfter', 233 | tab: 'myTab', 234 | tabTitle: 'myTabTitle', 235 | tabBeforeTitle: 'myTabBeforeTitle', 236 | tabAfterTitle: 'myTabAfterTitle', 237 | tabCloseIcon: 'myTabCloseIcon', 238 | tabBefore: 'myTabBefore', 239 | tabAfter: 'myTabAfter', 240 | tabActive: 'myTabActive' 241 | }; 242 | 243 | 244 | const tabs = [ 245 | ( 246 |
    247 |

    tab1Content

    248 |
    249 |
    ) 250 | ]; 251 | 252 | beforeEach(() => { 253 | component = ReactTestUtils.renderIntoDocument( 254 | ); 258 | }); 259 | 260 | it('render elements with custome class', function(){ 261 | 262 | let wrapper = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'rdTabWrapper'); 263 | expect(wrapper.className).contain('myWrapper'); 264 | 265 | let rdTabBar = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'rdTabBar'); 266 | expect(rdTabBar.className).contain('myTabBar'); 267 | 268 | let rdTabBarAfter = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'rdTabBarAfter'); 269 | expect(rdTabBarAfter.className).contain('myTabBarAfter'); 270 | 271 | let rdTab = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'rdTab'); 272 | expect(rdTab.className).contain('myTab'); 273 | 274 | let rdTabBefore = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'rdTabBefore'); 275 | expect(rdTabBefore.className).contain('myTabBefore'); 276 | 277 | let rdTabAfter = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'rdTabAfter'); 278 | expect(rdTabAfter.className).contain('myTabAfter'); 279 | 280 | let rdTabTitle = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'rdTabTitle'); 281 | expect(rdTabTitle.className).contain('myTabTitle'); 282 | 283 | let rdTabBeforeTitle = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'rdTabBeforeTitle'); 284 | expect(rdTabBeforeTitle.className).contain('myTabBeforeTitle'); 285 | 286 | let rdTabAfterTitle = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'rdTabAfterTitle'); 287 | expect(rdTabAfterTitle.className).contain('myTabAfterTitle'); 288 | 289 | let rdTabActive = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'rdTabActive'); 290 | expect(rdTabActive.className).contain('myTabActive'); 291 | 292 | let rdTabCloseIconb = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'rdTabCloseIcon'); 293 | expect(rdTabCloseIconb.className).contain('myTabCloseIcon'); 294 | 295 | }); 296 | 297 | }); 298 | 299 | 300 | describe('overwite inline style to all tabs', function(){ 301 | 302 | const tabsStyles = { 303 | tabWrapper: {fontSize: '100px'}, 304 | tabBar: {fontSize: '101px'}, 305 | tabBarAfter: {fontSize: '102px'}, 306 | tab: {fontSize: '103px'}, 307 | tabBefore: {fontSize: '104px'}, 308 | tabAfter: {fontSize: '105px'}, 309 | tabTitle: {fontSize: '106px'}, 310 | tabActive: {fontSize: '107px'}, 311 | tabTitleActive: {fontSize: '108px'}, 312 | tabBeforeActive: {fontSize: '109px'}, 313 | tabAfterActive: {fontSize: '110px'}, 314 | tabCloseIcon: {fontSize: '111px'}, 315 | tabOnHover: {fontSize: '112px'}, 316 | tabTitleOnHover: {fontSize: '113px'}, 317 | tabBeforeOnHover: {fontSize: '114px'}, 318 | tabAfterOnHover: {fontSize: '115px'} 319 | }; 320 | 321 | const tabs = [ 322 | ( 323 |
    324 |

    tab1Content

    325 |
    326 |
    ), 327 | ( 328 |
    329 |

    tab2Content

    330 |
    331 |
    ) 332 | ]; 333 | 334 | beforeEach(() => { 335 | component = ReactTestUtils.renderIntoDocument( 336 | ); 340 | }); 341 | 342 | it('render elements with custome inline-style', function(){ 343 | let rdTabWrapper = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'rdTabWrapper'); 344 | expect(rdTabWrapper.style.fontSize).to.be.eql('100px'); 345 | 346 | let rdTabBar = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'rdTabBar'); 347 | expect(rdTabBar.style.fontSize).to.be.eql('101px'); 348 | 349 | let rdTabBarAfter = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'rdTabBarAfter'); 350 | expect(rdTabBarAfter.style.fontSize).to.be.eql('102px'); 351 | 352 | let rdTab = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTab'); 353 | expect(rdTab[0].style.fontSize).to.be.eql('103px'); 354 | 355 | let rdTabBefore = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTabBefore'); 356 | expect(rdTabBefore[0].style.fontSize).to.be.eql('104px'); 357 | 358 | let rdTabAfter = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTabAfter'); 359 | expect(rdTabAfter[0].style.fontSize).to.be.eql('105px'); 360 | 361 | let rdTabTitle = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTabTitle'); 362 | expect(rdTabTitle[0].style.fontSize).to.be.eql('106px'); 363 | 364 | let activeTab = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'rdTabActive'); 365 | 366 | expect(activeTab.style.fontSize).to.be.eql('107px'); 367 | 368 | let rdTabTitleActive = activeTab.querySelector('.rdTabTitle'); 369 | expect(rdTabTitleActive.style.fontSize).to.be.eql('108px'); 370 | 371 | let rdTabBeforeActive = activeTab.querySelector('.rdTabBefore'); 372 | expect(rdTabBeforeActive.style.fontSize).to.be.eql('109px'); 373 | 374 | let rdTabAfterActive = activeTab.querySelector('.rdTabAfter'); 375 | expect(rdTabAfterActive.style.fontSize).to.be.eql('110px'); 376 | 377 | let rdTabCloseIcon = activeTab.querySelector('.rdTabCloseIcon'); 378 | expect(rdTabCloseIcon.style.fontSize).to.be.eql('111px'); 379 | 380 | }); 381 | 382 | }); 383 | 384 | describe('add custom className to specifyed tab ', function(){ 385 | 386 | const tabsClassNames = { 387 | tabBar: 'myTabBar', 388 | tabBarAfter: 'myTabBarAfter', 389 | tab: 'myTab', 390 | tabTitle: 'myTabTitle', 391 | tabBeforeTitle: 'myTabBeforeTitle', 392 | tabAfterTitle: 'myTabAfterTitle', 393 | tabCloseIcon: 'myTabCloseIcon', 394 | tabBefore: 'myTabBefore', 395 | tabAfter: 'myTabAfter', 396 | tabActive: 'myTabActive' 397 | }; 398 | 399 | const tabClassNames = { 400 | tab: 'mySpecialTab', 401 | tabTitle: 'mySpecialTabTitle', 402 | tabBeforeTitle: 'mySpecialTabBeforeTitle', 403 | tabAfterTitle: 'mySpecialTabAfterTitle', 404 | tabCloseIcon: 'mySpecialTabCloseIcon', 405 | tabBefore: 'mySpecialTabBefore', 406 | tabAfter: 'mySpecialTabAfter', 407 | tabActive: 'mySpecialTabActive' 408 | }; 409 | 410 | const tabs = [ 411 | ( 412 |
    413 |

    tab1Content

    414 |
    415 |
    ), 416 | ( 417 |
    418 |

    tab2Content

    419 |
    420 |
    ) 421 | ]; 422 | 423 | beforeEach(() => { 424 | component = ReactTestUtils.renderIntoDocument( 425 | ); 429 | }); 430 | 431 | it('render elements with custome class', function(){ 432 | let rdTabBar = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'rdTabBar'); 433 | expect(rdTabBar.className).contain('myTabBar'); 434 | 435 | let rdTabBarAfter = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'rdTabBarAfter'); 436 | expect(rdTabBarAfter.className).contain('myTabBarAfter'); 437 | 438 | let rdTab = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTab'); 439 | expect(rdTab[0].className).contain('mySpecialTab'); 440 | expect(rdTab[1].className).not.contain('mySpecialTab'); 441 | 442 | let rdTabBefore = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTabBefore'); 443 | expect(rdTabBefore[0].className).contain('mySpecialTabBefore'); 444 | expect(rdTabBefore[1].className).not.contain('mySpecialTabBefore'); 445 | 446 | let rdTabAfter = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTabAfter'); 447 | expect(rdTabAfter[0].className).contain('mySpecialTabAfter'); 448 | expect(rdTabAfter[1].className).not.contain('mySpecialTabAfter'); 449 | 450 | let rdTabTitle = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTabTitle'); 451 | expect(rdTabTitle[0].className).contain('mySpecialTabTitle'); 452 | expect(rdTabTitle[1].className).not.contain('mySpecialTabTitle'); 453 | 454 | let rdTabBeforeTitle = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTabBeforeTitle'); 455 | expect(rdTabBeforeTitle[0].className).contain('mySpecialTabBeforeTitle'); 456 | expect(rdTabBeforeTitle[1].className).not.contain('mySpecialTabBeforeTitle'); 457 | 458 | let rdTabAfterTitle = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTabAfterTitle'); 459 | expect(rdTabAfterTitle[0].className).contain('mySpecialTabAfterTitle'); 460 | expect(rdTabAfterTitle[1].className).not.contain('mySpecialTabAfterTitle'); 461 | 462 | let rdTabActive = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'rdTabActive'); 463 | expect(rdTabActive.className).contain('mySpecialTabActive'); 464 | 465 | let rdTabCloseIcon = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTabCloseIcon'); 466 | expect(rdTabCloseIcon[0].className).contain('mySpecialTabCloseIcon'); 467 | expect(rdTabCloseIcon[1].className).not.contain('mySpecialTabCloseIcon'); 468 | 469 | }); 470 | 471 | }); 472 | 473 | 474 | describe('overwite inline style to specifyed tab', function(){ 475 | 476 | const tabsStyles = { 477 | tabBar: {fontSize: '101px'}, 478 | tabBarAfter: {fontSize: '102px'}, 479 | tab: {fontSize: '103px'}, 480 | tabBefore: {fontSize: '104px'}, 481 | tabAfter: {fontSize: '105px'}, 482 | tabTitle: {fontSize: '106px'}, 483 | tabActive: {fontSize: '107px'}, 484 | tabTitleActive: {fontSize: '108px'}, 485 | tabBeforeActive: {fontSize: '109px'}, 486 | tabAfterActive: {fontSize: '110px'}, 487 | tabCloseIcon: {fontSize: '111px'} 488 | }; 489 | 490 | const tabStyles2 = { 491 | tab: {fontSize: '201px'}, 492 | tabBefore: {fontSize: '202px'}, 493 | tabAfter: {fontSize: '203px'}, 494 | tabTitle: {fontSize: '204px'}, 495 | tabActive: {fontSize: '205px'}, 496 | tabTitleActive: {fontSize: '206px'}, 497 | tabBeforeActive: {fontSize: '207px'}, 498 | tabAfterActive: {fontSize: '208px'}, 499 | tabCloseIcon: {fontSize: '209px'} 500 | }; 501 | 502 | const tabStyles3 = { 503 | tab: {fontSize: '301px'}, 504 | tabBefore: {fontSize: '302px'}, 505 | tabAfter: {fontSize: '303px'}, 506 | tabTitle: {fontSize: '304px'}, 507 | tabCloseIcon: {fontSize: '305px'} 508 | }; 509 | 510 | const tabs = [ 511 | ( 512 |
    513 |

    tab1Content

    514 |
    515 |
    ), 516 | ( 517 |
    518 |

    tab2Content

    519 |
    520 |
    ), 521 | ( 522 |
    523 |

    tab2Content

    524 |
    525 |
    ) 526 | 527 | ]; 528 | 529 | beforeEach(() => { 530 | component = ReactTestUtils.renderIntoDocument( 531 | ); 535 | }); 536 | 537 | it('render elements with custome inline-style', function(){ 538 | let rdTabBar = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'rdTabBar'); 539 | expect(rdTabBar.style.fontSize).to.be.eql('101px'); 540 | 541 | let rdTabBarAfter = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'rdTabBarAfter'); 542 | expect(rdTabBarAfter.style.fontSize).to.be.eql('102px'); 543 | 544 | let rdTab = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTab'); 545 | expect(rdTab[0].style.fontSize).to.be.eql('103px'); 546 | expect(rdTab[2].style.fontSize).to.be.eql('301px'); 547 | 548 | let rdTabBefore = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTabBefore'); 549 | expect(rdTabBefore[0].style.fontSize).to.be.eql('104px'); 550 | expect(rdTabBefore[2].style.fontSize).to.be.eql('302px'); 551 | 552 | let rdTabAfter = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTabAfter'); 553 | expect(rdTabAfter[0].style.fontSize).to.be.eql('105px'); 554 | expect(rdTabAfter[2].style.fontSize).to.be.eql('303px'); 555 | 556 | let rdTabTitle = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTabTitle'); 557 | expect(rdTabTitle[0].style.fontSize).to.be.eql('106px'); 558 | expect(rdTabTitle[2].style.fontSize).to.be.eql('304px'); 559 | 560 | let activeTab = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'rdTabActive'); 561 | 562 | expect(activeTab.style.fontSize).to.be.eql('205px'); 563 | 564 | let rdTabTitleActive = activeTab.querySelector('.rdTabTitle'); 565 | expect(rdTabTitleActive.style.fontSize).to.be.eql('206px'); 566 | 567 | let rdTabBeforeActive = activeTab.querySelector('.rdTabBefore'); 568 | expect(rdTabBeforeActive.style.fontSize).to.be.eql('207px'); 569 | 570 | let rdTabAfterActive = activeTab.querySelector('.rdTabAfter'); 571 | expect(rdTabAfterActive.style.fontSize).to.be.eql('208px'); 572 | 573 | let rdTabCloseIcon = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTabCloseIcon'); 574 | expect(rdTabCloseIcon[0].style.fontSize).to.be.eql('111px'); 575 | expect(rdTabCloseIcon[1].style.fontSize).to.be.eql('209px'); 576 | expect(rdTabCloseIcon[2].style.fontSize).to.be.eql('305px'); 577 | }); 578 | 579 | }); 580 | 581 | describe('when TabAddButton clicked', function () { 582 | let called = false; 583 | let currentTabs = []; 584 | 585 | const tabs = [ 586 | ( 587 |
    588 |

    tab1Content

    589 |
    590 |
    ), 591 | ( 592 |
    593 |

    tab2Content

    594 |
    595 |
    ) 596 | ]; 597 | 598 | before(() => { 599 | component = ReactTestUtils.renderIntoDocument( 600 | ); 604 | 605 | const button = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'rdTabAddButton'); 606 | ReactTestUtils.Simulate.click(button); 607 | }); 608 | 609 | it('should call onTabAddButtonClick prop', () => { 610 | expect(called).to.be.equal(true); 611 | }); 612 | it('should pass currentTabs to onTabAddButtonClick', () => { 613 | expect(currentTabs).to.be.length(2); 614 | expect(currentTabs[0].key).to.be.equal('tab1'); 615 | }); 616 | 617 | }); 618 | 619 | describe('when Tab clicked', function () { 620 | let called = false; 621 | let key = ''; 622 | let currentTabs = []; 623 | 624 | const tabs = [ 625 | ( 626 |
    627 |

    tab1Content

    628 |
    629 |
    ), 630 | ( 631 |
    632 |

    tab2Content

    633 |
    634 |
    ), 635 | ( 636 |
    637 |

    tab3Content

    638 |
    639 |
    ) 640 | ]; 641 | 642 | before(() => { 643 | component = ReactTestUtils.renderIntoDocument( 644 | ); 648 | 649 | let rdTabTitles = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTabTitle'); 650 | ReactTestUtils.Simulate.click(rdTabTitles[2]); 651 | }); 652 | 653 | it('should call onTabSelect prop', () => { 654 | expect(called).to.be.equal(true); 655 | }); 656 | it('should pass key and currentTabs to onTabSelect', () => { 657 | expect(key).to.be.eql('tab3'); 658 | 659 | expect(currentTabs).to.be.length(3); 660 | expect(currentTabs[0].key).to.be.equal('tab1'); 661 | }); 662 | }); 663 | 664 | describe('when Tab clicked', function () { 665 | let called = false; 666 | let key = ''; 667 | let currentTabs = []; 668 | 669 | const tabs = [ 670 | ( 671 |
    672 |

    tab1Content

    673 |
    674 |
    ), 675 | ( 676 |
    677 |

    tab2Content

    678 |
    679 |
    ), 680 | ( 681 |
    682 |

    tab3Content

    683 |
    684 |
    ) 685 | ]; 686 | 687 | before(() => { 688 | component = ReactTestUtils.renderIntoDocument( 689 | ); 693 | 694 | let rdTabTitles = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTabTitle'); 695 | ReactTestUtils.Simulate.click(rdTabTitles[2]); 696 | }); 697 | 698 | it('should call onTabSelect prop', () => { 699 | expect(called).to.be.equal(true); 700 | }); 701 | it('should pass key and currentTabs to onTabSelect', () => { 702 | expect(key).to.be.eql('tab3'); 703 | 704 | expect(currentTabs).to.be.length(3); 705 | expect(currentTabs[0].key).to.be.equal('tab1'); 706 | }); 707 | }); 708 | 709 | describe('when unselected Tab closed', function () { 710 | let called1 = false; 711 | let called2 = false; 712 | let key1 = ''; 713 | let key2 = ''; 714 | let currentTabs1 = []; 715 | let currentTabs2 = []; 716 | 717 | const tabs = [ 718 | ( 719 |
    720 |

    tab1Content

    721 |
    722 |
    ), 723 | ( 724 |
    725 |

    tab2Content

    726 |
    727 |
    ), 728 | ( 729 |
    730 |

    tab3Content

    731 |
    732 |
    ) 733 | ]; 734 | 735 | before(() => { 736 | component = ReactTestUtils.renderIntoDocument( 737 | ); 743 | 744 | let rdTabs = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTab'); 745 | const tab2CloseButton = rdTabs[1].querySelector('.myCloseButton'); 746 | ReactTestUtils.Simulate.click(tab2CloseButton); 747 | }); 748 | 749 | it('should call onTabClose prop', () => { 750 | expect(called1).to.be.equal(true); 751 | }); 752 | it('should pass key and currentTabs to onTabClose', () => { 753 | expect(key1).to.be.eql('tab2'); 754 | 755 | expect(currentTabs1).to.be.length(2); 756 | expect(currentTabs1[0].key).to.be.equal('tab1'); 757 | }); 758 | it('should selected tab will not change', () => { 759 | expect(called2).to.be.equal(false); 760 | expect(key2).to.be.eql(''); 761 | 762 | expect(currentTabs2).to.be.length(0); 763 | }); 764 | }); 765 | 766 | describe('when selected Tab closed', function () { 767 | let called1 = false; 768 | let called2 = false; 769 | let key1 = ''; 770 | let key2 = ''; 771 | let currentTabs1 = []; 772 | let currentTabs2 = []; 773 | 774 | const tabs = [ 775 | ( 776 |
    777 |

    tab1Content

    778 |
    779 |
    ), 780 | ( 781 |
    782 |

    tab2Content

    783 |
    784 |
    ), 785 | ( 786 |
    787 |

    tab3Content

    788 |
    789 |
    ) 790 | ]; 791 | 792 | before(() => { 793 | component = ReactTestUtils.renderIntoDocument( 794 | ); 800 | 801 | let rdTabs = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTab'); 802 | const tab2CloseButton = rdTabs[1].querySelector('.myCloseButton'); 803 | ReactTestUtils.Simulate.click(tab2CloseButton); 804 | }); 805 | 806 | it('should call onTabClose prop', () => { 807 | expect(called1).to.be.equal(true); 808 | }); 809 | it('should pass key and currentTabs to onTabClose', () => { 810 | expect(key1).to.be.eql('tab2'); 811 | 812 | expect(currentTabs1).to.be.length(2); 813 | expect(currentTabs1[0].key).to.be.equal('tab1'); 814 | }); 815 | it('should next tab will be active', () => { 816 | expect(called2).to.be.equal(true); 817 | expect(key2).to.be.eql('tab3'); 818 | 819 | expect(currentTabs2).to.be.length(2); 820 | expect(currentTabs2[0].key).to.be.equal('tab1'); 821 | }); 822 | }); 823 | 824 | describe('when last selected Tab closed', function () { 825 | let called1 = false; 826 | let called2 = false; 827 | let key1 = ''; 828 | let key2 = ''; 829 | let currentTabs1 = []; 830 | let currentTabs2 = []; 831 | 832 | const tabs = [ 833 | ( 834 |
    835 |

    tab1Content

    836 |
    837 |
    ), 838 | ( 839 |
    840 |

    tab2Content

    841 |
    842 |
    ), 843 | ( 844 |
    845 |

    tab3Content

    846 |
    847 |
    ) 848 | ]; 849 | 850 | before(() => { 851 | component = ReactTestUtils.renderIntoDocument( 852 | ); 858 | 859 | let rdTabs = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTab'); 860 | const tab3CloseButton = rdTabs[2].querySelector('.myCloseButton'); 861 | ReactTestUtils.Simulate.click(tab3CloseButton); 862 | }); 863 | 864 | it('should call onTabClose prop', () => { 865 | expect(called1).to.be.equal(true); 866 | }); 867 | it('should pass key and currentTabs to onTabClose', () => { 868 | expect(key1).to.be.eql('tab3'); 869 | 870 | expect(currentTabs1).to.be.length(2); 871 | expect(currentTabs1[0].key).to.be.equal('tab1'); 872 | }); 873 | it('should prev tab will be active', () => { 874 | expect(called2).to.be.equal(true); 875 | expect(key2).to.be.eql('tab2'); 876 | 877 | expect(currentTabs2).to.be.length(2); 878 | expect(currentTabs2[0].key).to.be.equal('tab1'); 879 | }); 880 | }); 881 | 882 | describe('when shouldTabClose return false', function () { 883 | let called1 = false; 884 | let called2 = false; 885 | let called3 = false; 886 | let key1 = ''; 887 | let key2 = ''; 888 | let key3 = ''; 889 | let currentTabs1 = []; 890 | let currentTabs2 = []; 891 | 892 | const tabs = [ 893 | ( 894 |
    895 |

    tab1Content

    896 |
    897 |
    ), 898 | ( 899 |
    900 |

    tab2Content

    901 |
    902 |
    ), 903 | ( 904 |
    905 |

    tab3Content

    906 |
    907 |
    ) 908 | ]; 909 | 910 | before(() => { 911 | component = ReactTestUtils.renderIntoDocument( 912 | ); 919 | 920 | let rdTabs = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTab'); 921 | const tab2CloseButton = rdTabs[1].querySelector('.myCloseButton'); 922 | ReactTestUtils.Simulate.click(tab2CloseButton); 923 | }); 924 | 925 | it('should not call onTabClose prop', () => { 926 | expect(called1).to.be.equal(false); 927 | }); 928 | it('should call shouldTabClose prop', () => { 929 | expect(called3).to.be.equal(true); 930 | }); 931 | it('should pass key to shouldTabClose', () => { 932 | expect(key3).to.be.eql('tab2'); 933 | }); 934 | it('should tabs stay ', () => { 935 | expect(called2).to.be.equal(false); 936 | }); 937 | }); 938 | 939 | describe('when dragged', function () { 940 | let called = false; 941 | let key = ''; 942 | let currentTabs = []; 943 | 944 | let target1; 945 | let target2; 946 | 947 | const tabs = [ 948 | ( 949 |
    950 |

    tab1Content

    951 |
    952 |
    ), 953 | ( 954 |
    955 |

    tab2Content

    956 |
    957 |
    ), 958 | ( 959 |
    960 |

    tab3Content

    961 |
    962 |
    ), 963 | ( 964 |
    965 |

    tab4Content

    966 |
    967 |
    ) 968 | 969 | ]; 970 | 971 | beforeEach(() => { 972 | ReactDom.render( 973 | , document.body); 978 | }); 979 | 980 | afterEach(() => { 981 | ReactDom.unmountComponentAtNode(document.body); 982 | target1 = null; 983 | target2 = null; 984 | called = false; 985 | key = ''; 986 | currentTabs = []; 987 | }); 988 | 989 | // 990 | it('should not call onTabPositionChanged when droped same position', () => { 991 | 992 | target1 = document.getElementsByClassName('rdTab')[0]; 993 | target2 = document.getElementsByClassName('rdTab')[0]; 994 | 995 | let clientY = target1.getBoundingClientRect().top + 5; 996 | 997 | triggerEvent(target1, 'mousedown', { 998 | clientX: target1.getBoundingClientRect().left + 5, 999 | clientY: clientY, 1000 | offset: { 1001 | left: target1.getBoundingClientRect().left + 10, 1002 | top: target1.getBoundingClientRect().top + 10 1003 | } 1004 | }); 1005 | 1006 | triggerEvent(target1, 'mousemove', { 1007 | clientX: target2.getBoundingClientRect().left + 10, 1008 | clientY: clientY 1009 | }); 1010 | 1011 | triggerEvent(target1, 'mouseup', { 1012 | clientX: target2.getBoundingClientRect().left + 10, 1013 | clientY: clientY 1014 | }); 1015 | 1016 | expect(called).to.be.equal(false); 1017 | }); 1018 | 1019 | it('swhich position when droped into next tab', () => { 1020 | 1021 | target1 = document.getElementsByClassName('rdTab')[0]; 1022 | target2 = document.getElementsByClassName('rdTab')[1]; 1023 | 1024 | let clientY = target1.getBoundingClientRect().top + 5; 1025 | let droppedToX = target2.getBoundingClientRect().left + 10; 1026 | 1027 | triggerEvent(target1, 'mousedown', { 1028 | clientX: target1.getBoundingClientRect().left + 5, 1029 | clientY: clientY, 1030 | offset: { 1031 | left: target1.getBoundingClientRect().left + 10, 1032 | top: target1.getBoundingClientRect().top + 10 1033 | } 1034 | }); 1035 | 1036 | triggerEvent(target1, 'mousemove', { 1037 | clientX: target2.getBoundingClientRect().left + 10, 1038 | clientY: clientY 1039 | }); 1040 | 1041 | triggerEvent(target1, 'mouseup', { 1042 | clientX: droppedToX, 1043 | clientY: clientY 1044 | }); 1045 | 1046 | expect(called).to.be.equal(true); 1047 | expect(key).to.be.equal('tab1'); 1048 | expect(currentTabs).to.be.length(4); 1049 | expect(currentTabs[0].key).to.be.equal('tab2'); 1050 | expect(currentTabs[1].key).to.be.equal('tab1'); 1051 | expect(currentTabs[2].key).to.be.equal('tab3'); 1052 | expect(currentTabs[3].key).to.be.equal('tab4'); 1053 | }); 1054 | 1055 | it('slide position when droped into next next tab', () => { 1056 | 1057 | target1 = document.getElementsByClassName('rdTab')[0]; 1058 | target2 = document.getElementsByClassName('rdTab')[2]; 1059 | 1060 | let clientY = target1.getBoundingClientRect().top + 5; 1061 | let droppedToX = target2.getBoundingClientRect().left + 10; 1062 | 1063 | triggerEvent(target1, 'mousedown', { 1064 | clientX: target1.getBoundingClientRect().left + 5, 1065 | clientY: clientY, 1066 | offset: { 1067 | left: target1.getBoundingClientRect().left + 10, 1068 | top: target1.getBoundingClientRect().top + 10 1069 | } 1070 | }); 1071 | 1072 | triggerEvent(target1, 'mousemove', { 1073 | clientX: target2.getBoundingClientRect().left + 10, 1074 | clientY: clientY 1075 | }); 1076 | 1077 | triggerEvent(target1, 'mouseup', { 1078 | clientX: droppedToX, 1079 | clientY: clientY 1080 | }); 1081 | 1082 | expect(called).to.be.equal(true); 1083 | expect(key).to.be.equal('tab1'); 1084 | expect(currentTabs).to.be.length(4); 1085 | expect(currentTabs[0].key).to.be.equal('tab2'); 1086 | expect(currentTabs[1].key).to.be.equal('tab3'); 1087 | expect(currentTabs[2].key).to.be.equal('tab1'); 1088 | expect(currentTabs[3].key).to.be.equal('tab4'); 1089 | }); 1090 | 1091 | 1092 | it('swhich position when droped into prev tab', () => { 1093 | 1094 | target1 = document.getElementsByClassName('rdTab')[1]; 1095 | target2 = document.getElementsByClassName('rdTab')[0]; 1096 | 1097 | let clientY = target1.getBoundingClientRect().top + 5; 1098 | let droppedToX = target2.getBoundingClientRect().left + 10; 1099 | 1100 | triggerEvent(target1, 'mousedown', { 1101 | clientX: target1.getBoundingClientRect().left + 5, 1102 | clientY: clientY, 1103 | offset: { 1104 | left: target1.getBoundingClientRect().left + 10, 1105 | top: target1.getBoundingClientRect().top + 10 1106 | } 1107 | }); 1108 | 1109 | triggerEvent(target1, 'mousemove', { 1110 | clientX: target2.getBoundingClientRect().left + 10, 1111 | clientY: clientY 1112 | }); 1113 | 1114 | triggerEvent(target1, 'mouseup', { 1115 | clientX: droppedToX, 1116 | clientY: clientY 1117 | }); 1118 | 1119 | expect(called).to.be.equal(true); 1120 | expect(key).to.be.equal('tab2'); 1121 | expect(currentTabs).to.be.length(4); 1122 | expect(currentTabs[0].key).to.be.equal('tab2'); 1123 | expect(currentTabs[1].key).to.be.equal('tab1'); 1124 | expect(currentTabs[2].key).to.be.equal('tab3'); 1125 | expect(currentTabs[3].key).to.be.equal('tab4'); 1126 | }); 1127 | 1128 | it('slide position when droped into prev prev tab', () => { 1129 | 1130 | target1 = document.getElementsByClassName('rdTab')[2]; 1131 | target2 = document.getElementsByClassName('rdTab')[0]; 1132 | 1133 | let clientY = target1.getBoundingClientRect().top + 5; 1134 | let droppedToX = target2.getBoundingClientRect().left + 10; 1135 | 1136 | triggerEvent(target1, 'mousedown', { 1137 | clientX: target1.getBoundingClientRect().left + 5, 1138 | clientY: clientY, 1139 | offset: { 1140 | left: target1.getBoundingClientRect().left + 10, 1141 | top: target1.getBoundingClientRect().top + 10 1142 | } 1143 | }); 1144 | 1145 | triggerEvent(target1, 'mousemove', { 1146 | clientX: target2.getBoundingClientRect().left + 10, 1147 | clientY: clientY 1148 | }); 1149 | 1150 | triggerEvent(target1, 'mouseup', { 1151 | clientX: droppedToX, 1152 | clientY: clientY 1153 | }); 1154 | 1155 | expect(called).to.be.equal(true); 1156 | expect(key).to.be.equal('tab3'); 1157 | expect(currentTabs).to.be.length(4); 1158 | expect(currentTabs[0].key).to.be.equal('tab3'); 1159 | expect(currentTabs[1].key).to.be.equal('tab1'); 1160 | expect(currentTabs[2].key).to.be.equal('tab2'); 1161 | expect(currentTabs[3].key).to.be.equal('tab4'); 1162 | }); 1163 | }) 1164 | 1165 | describe('when disabled and attempts to drag', function () { 1166 | let called = false; 1167 | 1168 | let target1; 1169 | let target2; 1170 | 1171 | const tabs = [ 1172 | ( 1173 |
    1174 |

    tab1Content

    1175 |
    1176 |
    ), 1177 | ( 1178 |
    1179 |

    tab2Content

    1180 |
    1181 |
    ), 1182 | ( 1183 |
    1184 |

    tab3Content

    1185 |
    1186 |
    ), 1187 | ( 1188 |
    1189 |

    tab4Content

    1190 |
    1191 |
    ) 1192 | 1193 | ]; 1194 | 1195 | beforeEach(() => { 1196 | ReactDom.render( 1197 | , document.body); 1205 | }); 1206 | 1207 | afterEach(() => { 1208 | ReactDom.unmountComponentAtNode(document.body); 1209 | target1 = null; 1210 | target2 = null; 1211 | called = false; 1212 | }); 1213 | 1214 | it('attemp to switch position when dragging is disabled', () => { 1215 | 1216 | target1 = document.getElementsByClassName('rdTab')[0]; 1217 | target2 = document.getElementsByClassName('rdTab')[1]; 1218 | 1219 | let clientY = target1.getBoundingClientRect().top + 5; 1220 | let droppedToX = target2.getBoundingClientRect().left + 10; 1221 | 1222 | triggerEvent(target1, 'mousedown', { 1223 | clientX: target1.getBoundingClientRect().left + 5, 1224 | clientY: clientY, 1225 | offset: { 1226 | left: target1.getBoundingClientRect().left + 10, 1227 | top: target1.getBoundingClientRect().top + 10 1228 | } 1229 | }); 1230 | 1231 | triggerEvent(target1, 'mousemove', { 1232 | clientX: target2.getBoundingClientRect().left + 10, 1233 | clientY: clientY 1234 | }); 1235 | 1236 | triggerEvent(target1, 'mouseup', { 1237 | clientX: droppedToX, 1238 | clientY: clientY 1239 | }); 1240 | 1241 | expect(called).to.be.equal(false); 1242 | }); 1243 | }); 1244 | 1245 | describe('when close shortcut key binded', function () { 1246 | let called = false; 1247 | let key = ''; 1248 | 1249 | const tabs = [ 1250 | ( 1251 |
    1252 |

    tab1Content

    1253 |
    1254 |
    ), 1255 | ( 1256 |
    1257 |

    tab2Content

    1258 |
    1259 |
    ), 1260 | ( 1261 |
    1262 |

    tab3Content

    1263 |
    1264 |
    ) 1265 | ]; 1266 | 1267 | before(() => { 1268 | component = ReactTestUtils.renderIntoDocument( 1269 | ); 1281 | Mousetrap.trigger('alt+command+w'); 1282 | }); 1283 | 1284 | it('keyboard event should call onTabClose prop', () => { 1285 | expect(called).to.be.equal(true); 1286 | expect(key).to.be.eql('tab1'); 1287 | }); 1288 | }); 1289 | 1290 | describe('when crateTab shortcut key binded', function () { 1291 | let called = false; 1292 | 1293 | const tabs = [ 1294 | ( 1295 |
    1296 |

    tab1Content

    1297 |
    1298 |
    ), 1299 | ( 1300 |
    1301 |

    tab2Content

    1302 |
    1303 |
    ), 1304 | ( 1305 |
    1306 |

    tab3Content

    1307 |
    1308 |
    ) 1309 | ]; 1310 | 1311 | before(() => { 1312 | component = ReactTestUtils.renderIntoDocument( 1313 | ); 1325 | Mousetrap.trigger('alt+command+t'); 1326 | }); 1327 | 1328 | it('keyboard event should call onTabAddButtonClick prop', () => { 1329 | expect(called).to.be.equal(true); 1330 | }); 1331 | }); 1332 | 1333 | describe('when moveRight shortcut key binded', function () { 1334 | let called = false; 1335 | let key = ''; 1336 | let currentTabs = []; 1337 | 1338 | const tabs = [ 1339 | ( 1340 |
    1341 |

    tab1Content

    1342 |
    1343 |
    ), 1344 | ( 1345 |
    1346 |

    tab2Content

    1347 |
    1348 |
    ), 1349 | ( 1350 |
    1351 |

    tab3Content

    1352 |
    1353 |
    ) 1354 | ]; 1355 | 1356 | before(() => { 1357 | component = ReactTestUtils.renderIntoDocument( 1358 | ); 1370 | Mousetrap.trigger('alt+command+tab'); 1371 | }); 1372 | 1373 | it('keyboard event should call tabSelect prop #1', () => { 1374 | expect(called).to.be.equal(true); 1375 | expect(key).to.be.equal('tab2'); 1376 | }); 1377 | 1378 | it('keyboard event should call tabSelect prop #2', () => { 1379 | Mousetrap.trigger('alt+command+tab'); 1380 | expect(key).to.be.equal('tab3'); 1381 | }); 1382 | 1383 | it('keyboard event should call tabSelect prop #3, and move to first if current tab is last', () => { 1384 | Mousetrap.trigger('alt+command+tab'); 1385 | expect(key).to.be.equal('tab1'); 1386 | }); 1387 | 1388 | }); 1389 | 1390 | describe('when moveLeft shortcut key binded', function () { 1391 | let called = false; 1392 | let key = ''; 1393 | let currentTabs = []; 1394 | 1395 | const tabs = [ 1396 | ( 1397 |
    1398 |

    tab1Content

    1399 |
    1400 |
    ), 1401 | ( 1402 |
    1403 |

    tab2Content

    1404 |
    1405 |
    ), 1406 | ( 1407 |
    1408 |

    tab3Content

    1409 |
    1410 |
    ) 1411 | ]; 1412 | 1413 | before(() => { 1414 | component = ReactTestUtils.renderIntoDocument( 1415 | ); 1427 | Mousetrap.trigger('shift+alt+command+tab'); 1428 | }); 1429 | 1430 | it('keyboard event should call tabSelect prop #1', () => { 1431 | expect(called).to.be.equal(true); 1432 | expect(key).to.be.equal('tab2'); 1433 | }); 1434 | 1435 | it('keyboard event should call tabSelect prop #2', () => { 1436 | Mousetrap.trigger('shift+alt+command+tab'); 1437 | expect(key).to.be.equal('tab1'); 1438 | }); 1439 | 1440 | it('keyboard event should call tabSelect prop #3, and move to last if current tab is first', () => { 1441 | Mousetrap.trigger('shift+alt+command+tab'); 1442 | expect(key).to.be.equal('tab3'); 1443 | }); 1444 | 1445 | }); 1446 | 1447 | 1448 | describe('handle mouseEnter/mouseLeave', () => { 1449 | const tabsClassNames = { 1450 | tabHover: 'myTabHover' 1451 | }; 1452 | const tab2ClassNames = { 1453 | tabHover: 'mySpecialTabHover' 1454 | }; 1455 | 1456 | const tabsStyles = { 1457 | tabOnHover: {fontSize: '101px'}, 1458 | tabBeforeOnHover: {fontSize: '102px'}, 1459 | tabAfterOnHover: {fontSize: '103px'}, 1460 | tabTitleOnHover: {fontSize: '104px'} 1461 | }; 1462 | 1463 | const tab2Styles = { 1464 | tabOnHover: {fontSize: '201px'}, 1465 | tabBeforeOnHover: {fontSize: '202px'}, 1466 | tabAfterOnHover: {fontSize: '203px'}, 1467 | tabTitleOnHover: {fontSize: '204px'} 1468 | }; 1469 | 1470 | const tabs = [ 1471 | ( 1472 |
    1473 |

    tab1Content

    1474 |
    1475 |
    ), 1476 | ( 1477 |
    1478 |

    tab2Content

    1479 |
    1480 |
    ), 1481 | ( 1482 |
    1483 |

    tab3Content

    1484 |
    1485 |
    ) 1486 | ]; 1487 | 1488 | beforeEach(() => { 1489 | component = ReactTestUtils.renderIntoDocument( 1490 | ); 1495 | }); 1496 | 1497 | it('should update style and className on mouseEnter', function () { 1498 | let rdTabs = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTab'); 1499 | ReactTestUtils.Simulate.mouseEnter(rdTabs[0]); 1500 | expect(ReactDom.findDOMNode(rdTabs[0]).classList.contains('rdTabHover')).to.be.equal(true); 1501 | expect(ReactDom.findDOMNode(rdTabs[0]).classList.contains('myTabHover')).to.be.equal(true); 1502 | expect(ReactDom.findDOMNode(rdTabs[0]).style.fontSize).to.be.equal('101px'); 1503 | 1504 | let rdTabBefore = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTabBefore'); 1505 | expect(rdTabBefore[0].style.fontSize).to.be.eql('102px'); 1506 | 1507 | let rdTabAfter = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTabAfter'); 1508 | expect(rdTabAfter[0].style.fontSize).to.be.eql('103px'); 1509 | 1510 | let rdTabTitle = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTabTitle'); 1511 | expect(rdTabTitle[0].style.fontSize).to.be.eql('104px'); 1512 | }); 1513 | 1514 | it('should update style and className on mouseLeave', function () { 1515 | let rdTabs = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTab'); 1516 | ReactTestUtils.Simulate.mouseLeave(rdTabs[0]); 1517 | expect(ReactDom.findDOMNode(rdTabs[0]).classList.contains('rdTabHover')).to.be.not.equal(true); 1518 | expect(ReactDom.findDOMNode(rdTabs[0]).classList.contains('myTabHover')).to.be.not.equal(true); 1519 | expect(ReactDom.findDOMNode(rdTabs[0]).style.fontSize).to.be.not.equal('101px'); 1520 | 1521 | let rdTabBefore = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTabBefore'); 1522 | expect(rdTabBefore[0].style.fontSize).to.be.not.eql('102px'); 1523 | 1524 | let rdTabAfter = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTabAfter'); 1525 | expect(rdTabAfter[0].style.fontSize).to.be.not.eql('103px'); 1526 | 1527 | let rdTabTitle = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTabTitle'); 1528 | expect(rdTabTitle[0].style.fontSize).to.be.not.eql('104px'); 1529 | }); 1530 | 1531 | it('should update style and className with tab customized value on mouseEnter', function () { 1532 | let rdTabs = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTab'); 1533 | ReactTestUtils.Simulate.mouseEnter(rdTabs[1]); 1534 | expect(ReactDom.findDOMNode(rdTabs[1]).classList.contains('rdTabHover')).to.be.equal(true); 1535 | expect(ReactDom.findDOMNode(rdTabs[1]).classList.contains('myTabHover')).to.be.equal(true); 1536 | expect(ReactDom.findDOMNode(rdTabs[1]).classList.contains('mySpecialTabHover')).to.be.equal(true); 1537 | expect(ReactDom.findDOMNode(rdTabs[1]).style.fontSize).to.be.equal('201px'); 1538 | 1539 | let rdTabBefore = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTabBefore'); 1540 | expect(rdTabBefore[1].style.fontSize).to.be.eql('202px'); 1541 | 1542 | let rdTabAfter = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTabAfter'); 1543 | expect(rdTabAfter[1].style.fontSize).to.be.eql('203px'); 1544 | 1545 | let rdTabTitle = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTabTitle'); 1546 | expect(rdTabTitle[1].style.fontSize).to.be.eql('204px'); 1547 | }); 1548 | 1549 | it('should not update style and className on mouseEnter over active tab', function () { 1550 | let rdTabs = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTab'); 1551 | ReactTestUtils.Simulate.mouseEnter(rdTabs[2]); 1552 | expect(ReactDom.findDOMNode(rdTabs[2]).classList.contains('rdTabHover')).to.be.not.equal(true); 1553 | expect(ReactDom.findDOMNode(rdTabs[2]).classList.contains('myTabHover')).to.be.not.equal(true); 1554 | expect(ReactDom.findDOMNode(rdTabs[2]).style.fontSize).to.be.not.equal('101px'); 1555 | 1556 | let rdTabBefore = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTabBefore'); 1557 | expect(rdTabBefore[2].style.fontSize).to.be.not.eql('102px'); 1558 | 1559 | let rdTabAfter = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTabAfter'); 1560 | expect(rdTabAfter[2].style.fontSize).to.be.not.eql('103px'); 1561 | 1562 | let rdTabTitle = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTabTitle'); 1563 | expect(rdTabTitle[2].style.fontSize).to.be.not.eql('104px'); 1564 | }); 1565 | }); 1566 | 1567 | describe('Event listeners for each Tab', function () { 1568 | let called1 = false; 1569 | let called2 = false; 1570 | let called3 = false; 1571 | let called4 = false; 1572 | 1573 | const tabs = [ 1574 | ( {called1 = true;}}> 1575 |
    1576 |

    tab1Content

    1577 |
    1578 |
    ), 1579 | ( {called2 = true;}} onContextMenu={() => {called3 = true;}}> 1580 |
    1581 |

    tab2Content

    1582 |
    1583 |
    ), 1584 | ( {called4 = true;}}> 1585 |
    1586 |

    tab3Content

    1587 |
    1588 |
    ) 1589 | ]; 1590 | let rdTabTitles; 1591 | 1592 | before(() => { 1593 | component = ReactTestUtils.renderIntoDocument( 1594 | ); 1597 | rdTabTitles = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'rdTabTitle'); 1598 | ReactTestUtils.Simulate.doubleClick(rdTabTitles[1]); 1599 | ReactTestUtils.Simulate.contextMenu(rdTabTitles[2]); 1600 | }); 1601 | 1602 | it('should call specified listener ', () => { 1603 | 1604 | expect(called1).to.be.equal(false); 1605 | expect(called2).to.be.equal(true); 1606 | expect(called3).to.be.equal(false); 1607 | expect(called4).to.be.equal(true); 1608 | }); 1609 | }); 1610 | 1611 | 1612 | 1613 | }); 1614 | -------------------------------------------------------------------------------- /test/helpers/styleOverride_spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import chai from 'chai'; 3 | let expect = chai.expect; 4 | import StyleOverride from '../../src/helpers/styleOverride'; 5 | 6 | describe('Test of styleOverride', () => { 7 | 8 | describe('styleOverride offer merge helper', () => { 9 | let src = { 10 | color:'red', 11 | fontSize:'10px' 12 | }; 13 | 14 | let src2 = { 15 | color: 'blue', 16 | textAlign: 'center' 17 | }; 18 | 19 | it('merge source style with other style', (done) => { 20 | let result = StyleOverride.merge(src, src2); 21 | expect(result).to.be.eql({ 22 | color: 'blue', 23 | fontSize:'10px', 24 | textAlign: 'center' 25 | }); 26 | done(); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/helpers/utils_spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import chai from 'chai'; 3 | let expect = chai.expect; 4 | import Utils from '../../src/helpers/utils'; 5 | 6 | describe('Test of utils', () => { 7 | 8 | describe('utils offer slideArray helper', () => { 9 | let src = ['a', 'b', 'c', 'd', 'e']; 10 | 11 | it('swap position of array element, if nextElement\'s index = fromElement index + 1', (done) => { 12 | let swapped = Utils.slideArray(src, 0, 1); 13 | expect(swapped).to.be.eql(['b', 'a', 'c', 'd', 'e']); 14 | 15 | let swapped2 = Utils.slideArray(src, 1, 2); 16 | expect(swapped2).to.be.eql(['a', 'c', 'b', 'd', 'e']); 17 | done(); 18 | }); 19 | 20 | it('swap position of array element, if nextElement\'s index = fromElement index - 1', (done) => { 21 | let swapped = Utils.slideArray(src, 1, 0); 22 | expect(swapped).to.be.eql(['b', 'a', 'c', 'd', 'e']); 23 | 24 | let swapped2 = Utils.slideArray(src, 2, 1); 25 | expect(swapped2).to.be.eql(['a', 'c', 'b', 'd', 'e']); 26 | done(); 27 | }); 28 | 29 | it('slide positions of array element, if nextElement\'s index > fromElement index + 1', (done) => { 30 | let swapped = Utils.slideArray(src, 0, 2); 31 | expect(swapped).to.be.eql(['b', 'c', 'a', 'd', 'e']); 32 | 33 | let swapped2 = Utils.slideArray(src, 1, 4); 34 | expect(swapped2).to.be.eql(['a', 'c', 'd', 'e', 'b']); 35 | done(); 36 | }); 37 | 38 | it('slide positions of array element, if nextElement\'s index > fromElement index - 1', (done) => { 39 | let swapped = Utils.slideArray(src, 2, 0); 40 | expect(swapped).to.be.eql(['c', 'a', 'b', 'd', 'e']); 41 | 42 | let swapped2 = Utils.slideArray(src, 4, 1); 43 | expect(swapped2).to.be.eql(['a', 'e', 'b', 'c', 'd']); 44 | done(); 45 | }); 46 | 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/triggerEvent.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://github.com/adamsanderson/trigger-event 3 | */ 4 | module.exports = trigger; 5 | 6 | /** 7 | Event type mappings. 8 | This is not an exhaustive list. 9 | */ 10 | var eventTypes = { 11 | load: 'HTMLEvents', 12 | unload: 'HTMLEvents', 13 | abort: 'HTMLEvents', 14 | error: 'HTMLEvents', 15 | select: 'HTMLEvents', 16 | change: 'HTMLEvents', 17 | submit: 'HTMLEvents', 18 | reset: 'HTMLEvents', 19 | focus: 'HTMLEvents', 20 | blur: 'HTMLEvents', 21 | resize: 'HTMLEvents', 22 | scroll: 'HTMLEvents', 23 | input: 'HTMLEvents', 24 | 25 | keyup: 'KeyboardEvent', 26 | keydown: 'KeyboardEvent', 27 | 28 | click: 'MouseEvents', 29 | dblclick: 'MouseEvents', 30 | mousedown: 'MouseEvents', 31 | mouseup: 'MouseEvents', 32 | mouseover: 'MouseEvents', 33 | mousemove: 'MouseEvents', 34 | mouseout: 'MouseEvents', 35 | contextmenu: 'MouseEvents' 36 | }; 37 | 38 | // Default event properties: 39 | var defaults = { 40 | clientX: 0, 41 | clientY: 0, 42 | button: 0, 43 | ctrlKey: false, 44 | altKey: false, 45 | shiftKey: false, 46 | metaKey: false, 47 | bubbles: true, 48 | cancelable: true, 49 | view: document.defaultView, 50 | key: '', 51 | location: 0, 52 | modifiers: '', 53 | repeat: 0, 54 | locale: '' 55 | }; 56 | 57 | /** 58 | * Trigger a DOM event. 59 | * 60 | * trigger(document.body, "click", {clientX: 10, clientY: 35}); 61 | * 62 | * Where sensible, sane defaults will be filled in. See the list of event 63 | * types for supported events. 64 | * 65 | * Loosely based on: 66 | * https://github.com/kangax/protolicious/blob/master/event.simulate.js 67 | */ 68 | function trigger(el, name, options){ 69 | var event, type; 70 | 71 | options = options || {}; 72 | for (var attr in defaults) { 73 | if (!options.hasOwnProperty(attr)) { 74 | options[attr] = defaults[attr]; 75 | } 76 | } 77 | 78 | if (document.createEvent) { 79 | // Standard Event 80 | type = eventTypes[name] || 'CustomEvent'; 81 | event = document.createEvent(type); 82 | initializers[type](el, name, event, options); 83 | el.dispatchEvent(event); 84 | } else { 85 | // IE Event 86 | event = document.createEventObject(); 87 | for (var key in options){ 88 | event[key] = options[key]; 89 | } 90 | el.fireEvent('on' + name, event); 91 | } 92 | } 93 | 94 | var initializers = { 95 | HTMLEvents: function(el, name, event, o){ 96 | return event.initEvent(name, o.bubbles, o.cancelable); 97 | }, 98 | 99 | KeyboardEvent: function(el, name, event, o){ 100 | // Use a blank key if not defined and initialize the charCode 101 | var key = ('key' in o) ? o.key : ""; 102 | var charCode; 103 | var modifiers; 104 | 105 | // 0 is the default location 106 | var location = ('location' in o) ? o.location : 0; 107 | 108 | if (event.initKeyboardEvent) { 109 | // Chrome and IE9+ uses initKeyboardEvent 110 | if (! 'modifiers' in o) { 111 | modifiers = []; 112 | if (o.ctrlKey) modifiers.push("Ctrl"); 113 | if (o.altKey) modifiers.push("Alt"); 114 | if (o.ctrlKey && o.altKey) modifiers.push("AltGraph"); 115 | if (o.shiftKey) modifiers.push("Shift"); 116 | if (o.metaKey) modifiers.push("Meta"); 117 | modifiers = modifiers.join(" "); 118 | } else { 119 | modifiers = o.modifiers; 120 | } 121 | 122 | return event.initKeyboardEvent( 123 | name, o.bubbles, o.cancelable, o.view, 124 | key, location, modifiers, o.repeat, o.locale 125 | ); 126 | } else { 127 | // Mozilla uses initKeyEvent 128 | charCode = ('charCode' in o) ? o.charCode : key.charCodeAt(0) || 0; 129 | return event.initKeyEvent( 130 | name, o.bubbles, o.cancelable, o.view, 131 | o.ctrlKey, o.altKey, o.shiftKey, 132 | o.metaKey, charCode, key 133 | ); 134 | } 135 | }, 136 | 137 | MouseEvents: function(el, name, event, o){ 138 | var screenX = ('screenX' in o) ? o.screenX : o.clientX; 139 | var screenY = ('screenY' in o) ? o.screenY : o.clientY; 140 | var clicks; 141 | var button; 142 | 143 | if ('detail' in o) { 144 | clicks = o.detail; 145 | } else if (name === 'dblclick') { 146 | clicks = 2; 147 | } else { 148 | clicks = 1; 149 | } 150 | 151 | // Default context menu to be a right click 152 | if (name === 'contextmenu') { 153 | button = button = o.button || 2; 154 | } 155 | 156 | return event.initMouseEvent(name, o.bubbles, o.cancelable, o.view, 157 | clicks, screenX, screenY, o.clientX, o.clientY, 158 | o.ctrlKey, o.altKey, o.shiftKey, o.metaKey, button, el); 159 | }, 160 | 161 | CustomEvent: function(el, name, event, o){ 162 | return event.initCustomEvent(name, o.bubbles, o.cancelable, o.detail); 163 | } 164 | }; --------------------------------------------------------------------------------