├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── __tests__ ├── .eslintrc ├── __snapshots__ │ ├── expiringStorage.test.js.snap │ └── tabs.test.js.snap ├── expiringStorage.test.js ├── helpers │ └── LocalStorageMock.js └── tabs.test.js ├── docs ├── CNAME ├── app.js ├── index.html ├── resources │ ├── app.js │ └── tabs-component.css └── webpack.config.js ├── package.json ├── src ├── components │ ├── Tab.vue │ └── Tabs.vue ├── expiringStorage.js └── index.js ├── webpack.base.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { 5 | "browsers": ["last 2 versions", "safari >= 7"] 6 | }, 7 | "modules": "umd" 8 | }] 9 | ], 10 | "plugins": [ 11 | "transform-object-rest-spread", 12 | ["transform-runtime", { 13 | "polyfill": false, 14 | "regenerator": true 15 | }] 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | indent_size = 4 9 | indent_style = space 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [{package.json,*.scss,*.css}] 15 | indent_size = 2 16 | 17 | [*.md] 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "spatie/vue", 3 | "parserOptions": { 4 | "ecmaVersion": 8 5 | }, 6 | "env": { 7 | "browser": true, 8 | "node": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | node_modules 3 | npm-debug.log 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | __tests__ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'stable' 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `vue-tabs-component` will be documented in this file 4 | 5 | ## 1.5.0 - 2018-XX-XX 6 | - Added `defaultTabHash` option 7 | - Fixed bug #32 `changed` event fires twice on each change 8 | - Added `clicked` event, fires when active tab is clicked 9 | - Fixed a bug which caused sections within tabs to have a '#' included in their id attribute. 10 | 11 | ## 1.4.0 - 2017-11-06 12 | - Added `isDisabled` prop to `Tab` 13 | 14 | ## 1.3.0 - 2017-10-06 15 | - Added `isVisible` prop to `Tab` 16 | 17 | ## 1.2.2 - 2017-10-04 18 | - Fixed default `useUrlFragment` behaviour 19 | 20 | ## 1.2.1 - 2017-10-04 21 | - Fixed bugs caused by unset options prop 22 | 23 | ## 1.2.0 - 2017-09-24 24 | - Added `useUrlFragment` option 25 | 26 | ## 1.1.0 - 2017-05-23 27 | - Added an alternative way to register components 28 | 29 | ## 1.0.0 - 2017-05-10 30 | - Initial release 31 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/spatie/vue-tabs-component). 6 | 7 | ## Pull Requests 8 | 9 | - Use the ES2015 syntax. 10 | - Your patch won't be accepted if it doesn't pass the tests and lints (`npm run test`). 11 | - If there's a `/demo` section, try to add an example. 12 | - **Document any change in behaviour:** Make sure the `README.md`, `CHANGELOG.md` and any other relevant documentation are kept up-to-date. 13 | - **Consider our release cycle:** We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 14 | - **Create feature branches:** Don't ask us to pull from your master branch. 15 | - **One pull request per feature:** If you want to do more than one thing, send multiple pull requests. 16 | - **Send coherent history:** Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 17 | 18 | ## Running Tests 19 | 20 | ``` bash 21 | jest 22 | ``` 23 | 24 | **Happy coding**! 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Spatie bvba 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 🚨 **THIS PACKAGE HAS BEEN ABANDONED** 🚨 2 | 3 | We don't use this package anymore in our own projects and cannot justify the time needed to maintain it anymore. That's why we have chosen to abandon it. Feel free to fork our code and maintain your own copy. 4 | 5 | # A Vue component to easily render tabs 6 | 7 | [![Latest Version on NPM](https://img.shields.io/npm/v/vue-tabs-component.svg?style=flat-square)](https://npmjs.com/package/vue-tabs-component) 8 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 9 | [![Build Status](https://img.shields.io/travis/spatie/vue-tabs-component/master.svg?style=flat-square)](https://travis-ci.org/spatie/vue-tabs-component) 10 | [![npm](https://img.shields.io/npm/dt/vue-tabs-component.svg?style=flat-square)](https://www.npmjs.com/package/vue-tabs-component) 11 | 12 | The package contains a [Vue](https://vuejs.org/) component to easily display some tabs. 13 | 14 | This is how they can be used: 15 | = 16 | ```html 17 |
18 | 19 | 20 | This is the content of the first tab 21 | 22 | 23 | This is the content of the second tab 24 | 25 | 26 | This content will be unavailable while :is-disabled prop set to true 27 | 28 | 29 | The fragment that is appended to the url can be customized 30 | 31 | 34 | A prefix and a suffix can be added 35 | 36 | 37 |
38 | ``` 39 | 40 | When reloading the page the component will automatically [display the tab that was previously opened](https://github.com/spatie/vue-tabs-component#remembering-the-last-opened-tab). 41 | 42 | The rendered output adheres to [the ARIA specification](http://heydonworks.com/practical_aria_examples/#tab-interface). 43 | 44 | ## Demo 45 | 46 | You can see a demo here: http://vue-tabs-component.spatie.be 47 | 48 | ## Installation 49 | 50 | You can install the package via yarn: 51 | 52 | ```bash 53 | yarn add vue-tabs-component 54 | ``` 55 | 56 | or npm: 57 | 58 | ```php 59 | npm install vue-tabs-component --save 60 | ``` 61 | 62 | ## Usage 63 | 64 | The most common use case is to register the component globally. 65 | 66 | ```js 67 | //in your app.js or similar file 68 | import Vue from 'vue'; 69 | import {Tabs, Tab} from 'vue-tabs-component'; 70 | 71 | Vue.component('tabs', Tabs); 72 | Vue.component('tab', Tab); 73 | ``` 74 | 75 | Alternatively you can do this to register the components: 76 | 77 | ```js 78 | import Tabs from 'vue-tabs-component'; 79 | 80 | Vue.use(Tabs); 81 | ``` 82 | 83 | On your page you can now use html like this to render tabs: 84 | 85 | ```html 86 |
87 | 88 | 89 | First tab content 90 | 91 | 92 | Second tab content 93 | 94 | 95 | Third tab content 96 | 97 | 98 |
99 | ``` 100 | 101 | By default it will show the first tab. 102 | 103 | If you click on a tab a `href` representation of the name will be append to the url. For example clicking on the tab `Second tab` will append `#second-tab` to the url. 104 | 105 | When loading a page with a fragment that matches the `href` of a tab, it will open up that tab. For example visiting `/#third-tab` will open up the tab with name `Third tab`. 106 | 107 | ### Remembering the last opened tab 108 | 109 | By default the component will remember which was the last open tab for 5 minutes . If you for instance click on `Third tab` and then visit `/` the third tab will be opened. 110 | 111 | You can change the cache life time by passing the lifetime in minutes in the `cache-lifetime` property of the `tabs` component. 112 | 113 | ```html 114 | 115 | ... 116 | 117 | ``` 118 | 119 | ### Disable modifying the url fragment 120 | 121 | When using with other libraries that use the url fragment, you can disable modifying the url fragment by passing the `useUrlFragment` options. This helps using it with vue-router, or using vue-tabs-component twice in the same page. 122 | 123 | ```html 124 | 125 | ... 126 | 127 | ``` 128 | 129 | ### Callbacks 130 | Tabs has two events to which you can bind: `changed` and `clicked` 131 | 132 | ```html 133 | 134 | ... 135 | 136 | ``` 137 | 138 | ```js 139 | export default { 140 | ... 141 | methods: { 142 | ... 143 | tabClicked (selectedTab) { 144 | console.log('Current tab re-clicked:' + selectedTab.tab.name); 145 | }, 146 | tabChanged (selectedTab) { 147 | console.log('Tab changed to:' + selectedTab.tab.name); 148 | }, 149 | ... 150 | } 151 | } 152 | ``` 153 | 154 | `changed` is emitted when the tab changes and can be used as handle to load data on request. 155 | `clicked` is emitted when an active tab is re-clicked and can be used to e.g. reload the data in the current tab. 156 | 157 | ### Adding a suffix and a prefix to the tab name 158 | 159 | You can add a suffix and a prefix to the tab by using the `suffix` and `prefix` attributes. 160 | 161 | ```html 162 | 163 | First tab content 164 | 165 | ``` 166 | 167 | The title of the tab will now be `my prefix - First tab - my suffix`. 168 | 169 | The fragment that's added to the url when clicking the tab will only be based on the `name` of a tab, the `name-prefix` and `name-suffix` attributes will be ignored. 170 | 171 | ### Customizing fragments 172 | 173 | When clicking on a tab it's name will be used as a fragment in the url. For example clicking on the `Second tab` will append `#second-tab` to the current url. 174 | 175 | You can customize that fragment by using the `id` attribute. 176 | 177 | ```html 178 |
179 | 180 | 181 | First tab content 182 | 183 | 184 |
185 | ``` 186 | 187 | Clicking on `My tab` will then append `#custom-fragment` to the url. 188 | 189 | 190 | ### Setting a default tab 191 | 192 | When disabling the cache, it can be useful to specify a default tab to load which is not the first one. You can select this by passing the `defaultTabHash` option. 193 | 194 | ```html 195 | 196 | 197 | First tab content 198 | 199 | 200 | Second tab content 201 | 202 | 203 | ``` 204 | 205 | ### CSS 206 | 207 | You can use the [CSS](docs/resources/tabs-component.css) from the docs as a starting point for your own styling. 208 | The output HTML has namespaced classes to target all nodes directly. 209 | 210 | ```html 211 |
212 | 217 |
218 |
219 | … 220 |
221 |
222 |
223 | ``` 224 | 225 | ## Changelog 226 | 227 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 228 | 229 | ## Testing 230 | 231 | ``` bash 232 | $ yarn test 233 | ``` 234 | 235 | ## Contributing 236 | 237 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 238 | 239 | ## Security 240 | 241 | If you discover any security related issues, please contact [Freek Van der Herten](https://github.com/freekmurze) instead of using the issue tracker. 242 | 243 | ## Postcardware 244 | 245 | You're free to use this package, but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. 246 | 247 | Our address is: Spatie, Samberstraat 69D, 2060 Antwerp, Belgium. 248 | 249 | We publish all received postcards [on our company website](https://spatie.be/en/opensource/postcards). 250 | 251 | ## Credits 252 | 253 | - [Freek Van der Herten](https://github.com/freekmurze) 254 | - [Willem Van Bockstal](https://github.com/willemvb) 255 | - [Sebastian De Deyne](https://github.com/sebastiandedeyne) 256 | - [All Contributors](../../contributors) 257 | 258 | This package is based on the solution presented by [Jeffrey Way](https://twitter.com/jeffrey_way) in the [practical example #3](https://laracasts.com/series/learn-vue-2-step-by-step/episodes/11) video in the [Vue series](https://vuecasts.com) on [Laracasts](https://laracasts.com) 259 | 260 | ## Support us 261 | 262 | Spatie is a webdesign agency based in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource). 263 | 264 | Does your business depend on our contributions? Reach out and support us on [Patreon](https://www.patreon.com/spatie). 265 | All pledges will be dedicated to allocating workforce on maintenance and new awesome stuff. 266 | 267 | ## License 268 | 269 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 270 | -------------------------------------------------------------------------------- /__tests__/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/expiringStorage.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`expiringStorage sets keys in the local storage 1`] = ` 4 | Object { 5 | "my-key": "{\\"value\\":\\"my-value\\",\\"expires\\":\\"2017-01-01T00:05:00.000Z\\"}", 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/tabs.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`vue-tabs-component can accept a prefix and a suffix for the name 1`] = ` 4 | " 5 |
6 | First tab content 7 |
8 | " 9 | `; 10 | 11 | exports[`vue-tabs-component can mount tabs 1`] = ` 12 | " 13 |
14 | First tab content 15 |
16 | Second tab content 17 |
18 | Third tab content 19 |
20 | " 21 | `; 22 | 23 | exports[`vue-tabs-component the life time of the cache can be set 1`] = ` 24 | Object { 25 | "vue-tabs-component.cache.blank": "{\\"value\\":\\"#first-tab\\",\\"expires\\":\\"2017-01-01T00:10:00.000Z\\"}", 26 | } 27 | `; 28 | 29 | exports[`vue-tabs-component uses the fragment of the url to determine which tab to open 1`] = ` 30 | " 31 |
32 | First tab content 33 |
34 | Second tab content 35 |
36 | Third tab content 37 |
38 | " 39 | `; 40 | 41 | exports[`vue-tabs-component writes the hash of the last opened tab in local storage 1`] = ` 42 | Object { 43 | "vue-tabs-component.cache.blank": "{\\"value\\":\\"#third-tab\\",\\"expires\\":\\"2017-01-01T00:05:00.000Z\\"}", 44 | } 45 | `; 46 | -------------------------------------------------------------------------------- /__tests__/expiringStorage.test.js: -------------------------------------------------------------------------------- 1 | import expiringStorage from '../src/expiringStorage'; 2 | import LocalStorageMock from './helpers/LocalStorageMock'; 3 | 4 | const localStorage = new LocalStorageMock(); 5 | 6 | window.localStorage = localStorage; 7 | 8 | describe('expiringStorage', () => { 9 | beforeEach(() => { 10 | localStorage.clear(); 11 | 12 | const dateClass = Date; 13 | 14 | // eslint-disable-next-line no-global-assign 15 | Date = function (dateString) { 16 | return new dateClass(dateString || '2017-01-01T00:00:00.000Z'); 17 | }; 18 | }); 19 | 20 | it('sets keys in the local storage', () => { 21 | expiringStorage.set('my-key', 'my-value', 5); 22 | 23 | expect(localStorage.getAll()).toMatchSnapshot(); 24 | }); 25 | 26 | it('remembers values by key', () => { 27 | expiringStorage.set('my-key', 'my-value', 5); 28 | 29 | expect(expiringStorage.get('my-key')).toEqual('my-value'); 30 | }); 31 | 32 | it('returns null if the value has expired ', () => { 33 | expiringStorage.set('my-key', 'my-value', 5); 34 | 35 | progressTime(5); 36 | 37 | expect(expiringStorage.get('my-key')).toEqual('my-value'); 38 | 39 | progressTime(1); 40 | 41 | expect(expiringStorage.get('my-key')).toBeNull(); 42 | }); 43 | 44 | it('returns null for unknown keys', () => { 45 | expect(expiringStorage.get('unknown-key')).toBeNull(); 46 | }); 47 | }); 48 | 49 | function progressTime(minutes) { 50 | const currentTime = (new Date()).getTime(); 51 | 52 | const newTime = new Date(currentTime + (minutes * 60000)); 53 | 54 | const originalDateClass = Date; 55 | 56 | // eslint-disable-next-line no-global-assign 57 | Date = function (dateString) { 58 | return new originalDateClass(dateString || newTime.toISOString()); 59 | }; 60 | } -------------------------------------------------------------------------------- /__tests__/helpers/LocalStorageMock.js: -------------------------------------------------------------------------------- 1 | export default class LocalStorageMock { 2 | constructor() { 3 | this.store = {}; 4 | } 5 | 6 | getAll() { 7 | return this.store; 8 | } 9 | 10 | getItem(key) { 11 | return this.store[key] || null; 12 | } 13 | 14 | setItem(key, value) { 15 | this.store[key] = value.toString(); 16 | } 17 | 18 | clear() { 19 | this.store = {}; 20 | } 21 | 22 | removeItem(key) { 23 | delete this.store[key]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /__tests__/tabs.test.js: -------------------------------------------------------------------------------- 1 | import { Tab, Tabs } from '../src'; 2 | import Vue from 'vue/dist/vue.js'; 3 | import expiringStorage from '../src/expiringStorage'; 4 | import LocalStorageMock from './helpers/LocalStorageMock'; 5 | 6 | const localStorage = new LocalStorageMock(); 7 | 8 | window.localStorage = localStorage; 9 | 10 | describe('vue-tabs-component', () => { 11 | Vue.component('tabs', Tabs); 12 | Vue.component('tab', Tab); 13 | 14 | beforeEach(() => { 15 | document.body.innerHTML = ` 16 |
17 | 18 | 19 | First tab content 20 | 21 | 22 | Second tab content 23 | 24 | 25 | Third tab content 26 | 27 | 28 |
29 | `; 30 | 31 | localStorage.clear(); 32 | 33 | const dateClass = Date; 34 | 35 | // eslint-disable-next-line no-global-assign 36 | Date = function (dateString) { 37 | return new dateClass(dateString || '2017-01-01T00:00:00.000Z'); 38 | }; 39 | 40 | window.location.hash = ''; 41 | }); 42 | 43 | it('can mount tabs', async () => { 44 | await createVm(); 45 | 46 | expect(document.body.innerHTML).toMatchSnapshot(); 47 | }); 48 | 49 | it('displays the first tab by default', async () => { 50 | const tabs = await createVm(); 51 | 52 | expect(tabs.activeTabHash).toEqual('#first-tab'); 53 | }); 54 | 55 | it('uses a custom fragment', async () => { 56 | document.body.innerHTML = ` 57 |
58 | 59 | 60 | First tab content 61 | 62 | 63 |
64 | `; 65 | 66 | const tabs = await createVm(); 67 | 68 | expect(tabs.activeTabHash).toEqual('#my-fragment'); 69 | }); 70 | 71 | it('uses the fragment of the url to determine which tab to open', async () => { 72 | window.location.hash = '#second-tab'; 73 | 74 | const tabs = await createVm(); 75 | 76 | expect(document.body.innerHTML).toMatchSnapshot(); 77 | }); 78 | 79 | it('ignores the fragment if it does not match the hash of a tab', async () => { 80 | window.location.hash = '#unknown-tab'; 81 | 82 | const tabs = await createVm(); 83 | 84 | expect(tabs.activeTabHash).toEqual('#first-tab'); 85 | }); 86 | 87 | it('writes the hash of the last opened tab in local storage', async () => { 88 | window.location.hash = '#third-tab'; 89 | 90 | const tabs = await createVm(); 91 | 92 | expect(tabs.activeTabHash).toEqual('#third-tab'); 93 | 94 | expect(localStorage.getAll()).toMatchSnapshot(); 95 | }); 96 | 97 | it('opens up the tabname found in storage', async () => { 98 | expiringStorage.set('vue-tabs-component.cache.blank', '#third-tab', 5); 99 | 100 | const tabs = await createVm(); 101 | 102 | expect(tabs.activeTabHash).toEqual('#third-tab'); 103 | }); 104 | 105 | it('will not use the tab in storage if it has expired', async () => { 106 | expiringStorage.set('vue-tabs-component.cache.blank', '#third-tab', 5); 107 | 108 | progressTime(6); 109 | 110 | const tabs = await createVm(); 111 | 112 | expect(tabs.activeTabHash).toEqual('#first-tab'); 113 | }); 114 | 115 | it('the life time of the cache can be set', async () => { 116 | document.body.innerHTML = ` 117 |
118 | 119 | 120 | First tab content 121 | 122 | 123 |
124 | `; 125 | 126 | await createVm(); 127 | 128 | expect(localStorage.getAll()).toMatchSnapshot(); 129 | }); 130 | 131 | it('can accept a prefix and a suffix for the name', async () => { 132 | document.body.innerHTML = ` 133 |
134 | 135 | 136 | First tab content 137 | 138 | 139 |
140 | `; 141 | 142 | await createVm(); 143 | 144 | expect(document.body.innerHTML).toMatchSnapshot(); 145 | }); 146 | }); 147 | 148 | async function createVm() { 149 | const vm = new Vue({ 150 | el: '#app', 151 | }); 152 | 153 | await Vue.nextTick(); 154 | 155 | return vm.$children[0]; 156 | } 157 | 158 | function progressTime(minutes) { 159 | const currentTime = (new Date()).getTime(); 160 | 161 | const newTime = new Date(currentTime + (minutes * 60000)); 162 | 163 | const originalDateClass = Date; 164 | 165 | // eslint-disable-next-line no-global-assign 166 | Date = function (dateString) { 167 | return new originalDateClass(dateString || newTime.toISOString()); 168 | }; 169 | } 170 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | vue-tabs-component.spatie.be -------------------------------------------------------------------------------- /docs/app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { Tab, Tabs } from '../src'; 3 | 4 | new Vue({ 5 | components: { Tab, Tabs }, 6 | 7 | el: '#app', 8 | }); 9 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | vue-tabs-component 4 | 5 | 6 | 7 | 141 | 142 | 143 | 144 |
145 |

146 | Vue-tabs-component 147 |

148 | 149 | 150 |

First tab

151 | This is the content of the first tab. 152 |
153 | 154 |

Second tab

155 | This is the content of the second tab. 156 |
157 | 158 |

Disabled tab

159 | This content will be unavailable while :is-disabled prop set to true 160 |
161 | 162 |

Custom fragment

163 | The hash that is appended to the url can be customized. 164 |
165 | 166 |

Prefix and suffix

167 | A prefix and a suffix can be added — HTML allowed. 168 |
169 |
170 | 171 |
172 |

About

173 |

174 | The tab component above is powered by spatie/vue-tabs-component. 175 |

176 | 177 |

The component will use the fragment of the url to choose which tab to open. So clicking 178 | #second-tab 179 | will display the contents of the tab named Second tab.

180 | 181 |

The component will also remember 182 | which tab was opened previously. If you reload without fragment 183 | the tab that is currently active will receive focus again. More about this feature on Github. 184 |

185 |
186 | 187 | 188 | Code on Github | 189 | Proudly presented by spatie.be 190 | 191 |
192 | 193 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /docs/resources/tabs-component.css: -------------------------------------------------------------------------------- 1 | .tabs-component { 2 | margin: 4em 0; 3 | } 4 | 5 | .tabs-component-tabs { 6 | border: solid 1px #ddd; 7 | border-radius: 6px; 8 | margin-bottom: 5px; 9 | } 10 | 11 | @media (min-width: 700px) { 12 | .tabs-component-tabs { 13 | border: 0; 14 | align-items: stretch; 15 | display: flex; 16 | justify-content: flex-start; 17 | margin-bottom: -1px; 18 | } 19 | } 20 | 21 | .tabs-component-tab { 22 | color: #999; 23 | font-size: 14px; 24 | font-weight: 600; 25 | margin-right: 0; 26 | list-style: none; 27 | } 28 | 29 | .tabs-component-tab:not(:last-child) { 30 | border-bottom: dotted 1px #ddd; 31 | } 32 | 33 | .tabs-component-tab:hover { 34 | color: #666; 35 | } 36 | 37 | .tabs-component-tab.is-active { 38 | color: #000; 39 | } 40 | 41 | .tabs-component-tab.is-disabled * { 42 | color: #cdcdcd; 43 | cursor: not-allowed !important; 44 | } 45 | 46 | @media (min-width: 700px) { 47 | .tabs-component-tab { 48 | background-color: #fff; 49 | border: solid 1px #ddd; 50 | border-radius: 3px 3px 0 0; 51 | margin-right: .5em; 52 | transform: translateY(2px); 53 | transition: transform .3s ease; 54 | } 55 | 56 | .tabs-component-tab.is-active { 57 | border-bottom: solid 1px #fff; 58 | z-index: 2; 59 | transform: translateY(0); 60 | } 61 | } 62 | 63 | .tabs-component-tab-a { 64 | align-items: center; 65 | color: inherit; 66 | display: flex; 67 | padding: .75em 1em; 68 | text-decoration: none; 69 | } 70 | 71 | .tabs-component-panels { 72 | padding: 4em 0; 73 | } 74 | 75 | @media (min-width: 700px) { 76 | .tabs-component-panels { 77 | border-top-left-radius: 0; 78 | background-color: #fff; 79 | border: solid 1px #ddd; 80 | border-radius: 0 6px 6px 6px; 81 | box-shadow: 0 0 10px rgba(0, 0, 0, .05); 82 | padding: 4em 2em; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /docs/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const merge = require('webpack-merge'); 3 | 4 | module.exports = merge(require('../webpack.base'), { 5 | context: __dirname, 6 | 7 | entry: './app.js', 8 | 9 | output: { 10 | path: path.resolve(__dirname, 'resources'), 11 | filename: 'app.js', 12 | publicPath: '/resources/', 13 | }, 14 | 15 | resolve: { 16 | alias: { 17 | vue: 'vue/dist/vue.js', 18 | }, 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-tabs-component", 3 | "version": "1.5.0", 4 | "description": "A Vue component to easily render tabs", 5 | "main": "dist/index.js", 6 | "jsnext:main": "src/index.js", 7 | "scripts": { 8 | "start": "webpack-dev-server --config docs/webpack.config.js", 9 | "demo": "NODE_ENV=production webpack --config docs/webpack.config.js", 10 | "build": "rm -rf dist && NODE_ENV=production webpack", 11 | "lint": "eslint --ext .js,.vue --fix src __tests__; exit 0", 12 | "prepublish": "npm run test; npm run build", 13 | "test": "jest" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/spatie/vue-tabs-component.git" 18 | }, 19 | "keywords": [ 20 | "spatie" 21 | ], 22 | "author": "Freek Van der Herten", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/spatie/vue-tabs-component/issues" 26 | }, 27 | "homepage": "https://github.com/spatie/vue-tabs-component", 28 | "peerDependencies": { 29 | "vue": "^2.3.0" 30 | }, 31 | "devDependencies": { 32 | "babel-core": "^6.24.1", 33 | "babel-loader": "^7.0.0", 34 | "babel-plugin-transform-object-rest-spread": "^6.16.0", 35 | "babel-plugin-transform-runtime": "^6.23.0", 36 | "babel-preset-env": "^1.4.0", 37 | "css-loader": "^0.28.1", 38 | "eslint": "^3.7.1", 39 | "eslint-config-spatie": "^1.1.0", 40 | "jest": "^19.0.0", 41 | "jest-vue-preprocessor": "^0.2.0", 42 | "vue": "^2.3.0", 43 | "vue-loader": "^12.0.3", 44 | "vue-template-compiler": "^2.3.0", 45 | "webpack": "^3.12.0", 46 | "webpack-merge": "^4.1.0" 47 | }, 48 | "jest": { 49 | "testRegex": "test.js$", 50 | "moduleFileExtensions": [ 51 | "js", 52 | "vue" 53 | ], 54 | "transform": { 55 | "^.+\\.js$": "/node_modules/babel-jest", 56 | ".*\\.(vue)$": "/node_modules/jest-vue-preprocessor" 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/components/Tab.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 46 | -------------------------------------------------------------------------------- /src/components/Tabs.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 180 | -------------------------------------------------------------------------------- /src/expiringStorage.js: -------------------------------------------------------------------------------- 1 | class ExpiringStorage { 2 | get(key) { 3 | const cached = JSON.parse( 4 | localStorage.getItem(key) 5 | ); 6 | 7 | if (! cached) { 8 | return null; 9 | } 10 | 11 | const expires = new Date(cached.expires); 12 | 13 | if (expires < new Date()) { 14 | localStorage.removeItem(key); 15 | return null; 16 | } 17 | 18 | return cached.value; 19 | } 20 | 21 | set(key, value, lifeTimeInMinutes) { 22 | const currentTime = new Date().getTime(); 23 | 24 | const expires = new Date(currentTime + lifeTimeInMinutes * 60000); 25 | 26 | localStorage.setItem(key, JSON.stringify({ value, expires })); 27 | } 28 | } 29 | 30 | export default new ExpiringStorage(); 31 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Tab from './components/Tab'; 2 | import Tabs from './components/Tabs'; 3 | 4 | export default { 5 | install(Vue) { 6 | Vue.component('tab', Tab); 7 | Vue.component('tabs', Tabs); 8 | }, 9 | }; 10 | 11 | export { Tab, Tabs }; 12 | -------------------------------------------------------------------------------- /webpack.base.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | module: { 3 | rules: [ 4 | { 5 | test: /\.js/, 6 | loaders: ['babel-loader'], 7 | exclude: /node_modules/, 8 | }, 9 | { 10 | test: /\.vue$/, 11 | loaders: ['vue-loader'], 12 | exclude: /node_modules/, 13 | }, 14 | ], 15 | }, 16 | 17 | resolve: { 18 | extensions: ['.js', '.vue'], 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const merge = require('webpack-merge'); 3 | 4 | module.exports = merge(require('./webpack.base'), { 5 | context: __dirname, 6 | 7 | entry: './src/index.js', 8 | 9 | output: { 10 | path: path.resolve(__dirname, 'dist'), 11 | filename: 'index.js', 12 | library: { 13 | root: 'VueTabs', 14 | amd: 'vue-tabs', 15 | commonjs: 'vue-tabs' 16 | }, 17 | libraryTarget: 'umd', 18 | }, 19 | 20 | externals: { 21 | vue: 'vue', 22 | }, 23 | }); 24 | --------------------------------------------------------------------------------