├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── .gitignore ├── .markdownlint.json ├── .prettierrc.json ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── .nojekyll ├── assets │ ├── css │ │ └── main.css │ └── img │ │ ├── github.svg │ │ ├── npm.svg │ │ └── twitter.svg ├── index.html ├── index.md └── sidebar.md ├── eslint.config.js ├── package-lock.json ├── package.json ├── postcss.config.cjs ├── prettierignore ├── rollup.config.js ├── screenshot.jpg ├── server.js └── src ├── css └── vars.css ├── js └── index.js └── scss └── style.scss /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: jhildenbiddle 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: 'Build' 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: Build (${{ matrix.os }}) 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | matrix: 11 | os: [macos-latest, ubuntu-latest, windows-latest] 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Install 17 | run: npm ci 18 | 19 | - name: Lint 20 | run: npm run lint 21 | 22 | - name: Build 23 | run: npm run build 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Folders 2 | dist 3 | node_modules 4 | 5 | # Files 6 | *.log 7 | 8 | # OS 9 | ._* 10 | .cache 11 | .DS_Store 12 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "MD001": false, 4 | "MD004": { "style": "consistent" }, 5 | "MD013": false, 6 | "MD023": false, 7 | "MD024": false, 8 | "MD033": false, 9 | "MD036": false 10 | } 11 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "printWidth": 100, 4 | "singleQuote": true, 5 | "trailingComma": "none" 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["Codacy", "jhildenbiddle", "themeable"] 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 1.6.3 4 | 5 | _2024-04-24_ 6 | 7 | - Fix tab heading anchors when using history routerMode (#55) 8 | 9 | ## 1.6.2 10 | 11 | _2024-02-29_ 12 | 13 | - Fix tab selection on reload (#51) 14 | 15 | ## 1.6.1 16 | 17 | _2024-02-06_ 18 | 19 | - Fix GitHub workflow badge 20 | 21 | ## 1.6.0 22 | 23 | _2022-09-11_ 24 | 25 | - Add support for nested tabs (#5) 26 | - Fix tab content margin of first & last element 27 | - Update sessionStorage keys to include namespace 28 | 29 | ## 1.5.4 30 | 31 | _2022-09-10_ 32 | 33 | - Fix inactive tab content flicker on tab change (#27) 34 | - Fix tab parsing with compressed HTML (#45) 35 | 36 | ## 1.5.3 37 | 38 | _2022-07-29_ 39 | 40 | - Fix plugin insertion point (fix for docsify-mustache) (#44) 41 | - Update dependencies 42 | 43 | ## 1.5.2 44 | 45 | _2021-09-02_ 46 | 47 | - Fix code quality badges 48 | - Add GitHub CI 49 | 50 | ## 1.5.1 51 | 52 | _2021-09-02_ 53 | 54 | - Fix setting active tab from anchor with unicode (#41) 55 | 56 | ## 1.5.0 57 | 58 | _2021-04-27_ 59 | 60 | - Add support for markdown and HTML in tab labels (#38) 61 | - Update custom style examples in documentation 62 | 63 | ## 1.4.4 64 | 65 | _2020-11-05_ 66 | 67 | - Fix tab comments with code block rendering (#29) 68 | 69 | ## 1.4.3 70 | 71 | _2020-06-25_ 72 | 73 | - Fix handling of regex replacement patterns in markdown (#26) 74 | 75 | ## 1.4.2 76 | 77 | _2020-05-11_ 78 | 79 | - Fix error when no active tab set in URL (#23) 80 | 81 | ## 1.4.1 82 | 83 | _2020-05-09_ 84 | 85 | - Fix handling of URL encoded anchor IDs (#22) 86 | 87 | ## 1.4.0 88 | 89 | _2020-04-12_ 90 | 91 | - Add tab selection on hash change 92 | - Fix tab selection based on anchor ID in IE 93 | 94 | ## 1.3.0 95 | 96 | _2020-04-11_ 97 | 98 | - Add tab selection based on anchor ID in URL (#20) 99 | - Fix tab content container padding and first/last child margins 100 | 101 | ## 1.2.0 102 | 103 | _2020-02-11_ 104 | 105 | - Update sync behavior to allow synced tab selections across pages (#17) 106 | - Fix rendering of tabset when using tab comments (#16) 107 | - Remove Sentry.io 108 | 109 | ## 1.1.2 110 | 111 | _2019-01-08_ 112 | 113 | - Add Sentry.io 114 | - Update dependencies 115 | - Update CDN links (switch from unpkg to jsdelivr) 116 | - Fix rollup plugin configuration 117 | - Fix website landscape display on notched devices 118 | 119 | ## 1.1.0 120 | 121 | _2018-11-10_ 122 | 123 | - Add support for tabsets nested within lists 124 | 125 | ## 1.0.6 126 | 127 | _2018-11-01_ 128 | 129 | - Fix rendering issue caused by marked package upgrade in docsify 4.8.0 130 | 131 | ## 1.0.5 132 | 133 | _2018-10-11_ 134 | 135 | - Fix bug that prevented rendering of tabs from markdown with Windows-style 136 | line terminators 137 | 138 | ## 1.0.0 - 1.0.4 139 | 140 | _2018-10-09_ 141 | 142 | - Initial release 143 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 John Hildenbiddle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docsify-tabs 2 | 3 | [![NPM](https://img.shields.io/npm/v/docsify-tabs.svg?style=flat-square)](https://www.npmjs.com/package/docsify-tabs) 4 | [![GitHub Workflow Status (master)](https://img.shields.io/github/actions/workflow/status/jhildenbiddle/docsify-tabs/test.yml?branch=master&label=checks&style=flat-square)](https://github.com/jhildenbiddle/docsify-tabs/actions?query=branch%3Amaster+) 5 | [![Codacy grade](https://img.shields.io/codacy/grade/e9c2a9504211450ab39e0d72a1158a47.svg?style=flat-square)](https://app.codacy.com/gh/jhildenbiddle/docsify-tabs/dashboard) 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://github.com/jhildenbiddle/docsify-tabs/blob/master/LICENSE) 7 | [![jsDelivr](https://data.jsdelivr.com/v1/package/npm/docsify-tabs/badge)](https://www.jsdelivr.com/package/npm/docsify-tabs) 8 | [![Sponsor this project](https://img.shields.io/static/v1?style=flat-square&label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/jhildenbiddle) 9 | 10 | A [docsify.js](https://docsify.js.org) plugin for rendering tabbed content from markdown. 11 | 12 | - [Documentation & Demos](https://jhildenbiddle.github.io/docsify-tabs) 13 | 14 |

15 | 16 | Screenshot 17 | 18 |

19 | 20 | > 💡 Like this plugin? Check out [docsify-themeable](https://jhildenbiddle.github.io/docsify-themeable) for your site theme, [docsify-plugin-ethicalads](https://jhildenbiddle.github.io/docsify-plugin-ethicalads/) for EthicalAds integration, and [docsify-plugin-runkit](https://jhildenbiddle.github.io/docsify-plugin-runkit/) for live JavaScript REPLs! 21 | 22 | ## Features 23 | 24 | - Generate tabbed content using unobtrusive markup 25 | - Persist tab selections on refresh/revisit 26 | - Sync tab selection for tabs with matching labels 27 | - Nest tab sets within tab sets 28 | - Style tabs using "classic" or "material" tab theme 29 | - Customize styles without complex CSS using CSS custom properties 30 | - Compatible with [docsify-themeable](https://jhildenbiddle.github.io/docsify-themeable/) themes 31 | 32 | **Limitations** 33 | 34 | - Tabs wraps when their combined width exceeds the content area width 35 | 36 | ## Installation & Options 37 | 38 | See the [documentation site](https://jhildenbiddle.github.io/docsify-tabs) for details. 39 | 40 | ## Sponsorship 41 | 42 | A [sponsorship](https://github.com/sponsors/jhildenbiddle) is more than just a way to show appreciation for the open-source authors and projects we rely on; it can be the spark that ignites the next big idea, the inspiration to create something new, and the motivation to share so that others may benefit. 43 | 44 | If you benefit from this project, please consider lending your support and encouraging future efforts by [becoming a sponsor](https://github.com/sponsors/jhildenbiddle). 45 | 46 | Thank you! 🙏🏻 47 | 48 | ## Contact & Support 49 | 50 | - Follow 👨🏻‍💻 **@jhildenbiddle** on [Twitter](https://twitter.com/jhildenbiddle) and [GitHub](https://github.com/jhildenbiddle) for announcements 51 | - Create a 💬 [GitHub issue](https://github.com/jhildenbiddle/docsify-tabs/issues) for bug reports, feature requests, or questions 52 | - Add a ⭐️ [star on GitHub](https://github.com/jhildenbiddle/docsify-tabs) and 🐦 [tweet](https://twitter.com/intent/tweet?url=https%3A%2F%2Fgithub.com%2Fjhildenbiddle%2Fdocsify-tabs&hashtags=css,developers,frontend,javascript) to promote the project 53 | - Become a 💖 [sponsor](https://github.com/sponsors/jhildenbiddle) to support the project and future efforts 54 | 55 | ## License 56 | 57 | This project is licensed under the MIT License. See the [LICENSE](https://github.com/jhildenbiddle/docsify-tabs/blob/master/LICENSE) for details. 58 | 59 | Copyright (c) John Hildenbiddle ([@jhildenbiddle](https://twitter.com/jhildenbiddle)) 60 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhildenbiddle/docsify-tabs/ecc0184c042627961cb54882f1a43adecff0577e/docs/.nojekyll -------------------------------------------------------------------------------- /docs/assets/css/main.css: -------------------------------------------------------------------------------- 1 | /* Required for browsers w/o shadow DOM support */ 2 | iframe[src*='buttons.github.io'] { 3 | margin: 0; 4 | } 5 | 6 | .markdown-section strong code { 7 | font-weight: normal; 8 | } 9 | 10 | /* Theme Toggles */ 11 | label[data-class-target='label + .docsify-tabs'] { 12 | margin-right: 0.8em; 13 | } 14 | 15 | /* Custom Styles */ 16 | /* ========================================================================== */ 17 | /* Badges */ 18 | .tab-badge, 19 | [data-tab='badge']:after { 20 | position: absolute; 21 | top: 0; 22 | right: 0; 23 | transform: translate(35%, -45%); 24 | padding: 0.25em 0.35em; 25 | border-radius: 3px; 26 | background: red; 27 | color: white; 28 | font-family: sans-serif; 29 | font-size: 11px; 30 | font-weight: bold; 31 | } 32 | 33 | [data-tab='badge']:after { 34 | content: 'New!'; 35 | } 36 | 37 | /* Active State */ 38 | .docsify-tabs__tab--active[data-tab='active state'] { 39 | box-shadow: none; 40 | background: #13547a; 41 | color: white; 42 | } 43 | .docsify-tabs__content[data-tab-content='active state'] { 44 | background-image: linear-gradient(0deg, #80d0c7 0%, #13547a 100%); 45 | } 46 | .docsify-tabs__content[data-tab-content='active state'] p strong { 47 | color: white; 48 | } 49 | 50 | /* CodePen */ 51 | [data-tab-content='codepen'] .cp_embed_wrapper { 52 | position: relative; 53 | top: calc(0px - var(--docsifytabs-content-padding)); 54 | left: calc(0px - var(--docsifytabs-content-padding)); 55 | width: calc(100% + calc(var(--docsifytabs-content-padding) * 2)); 56 | margin-bottom: calc(0px - var(--docsifytabs-content-padding)); 57 | } 58 | 59 | [data-tab-content='codepen'] .cp_embed_wrapper > * { 60 | margin: 0; 61 | } 62 | -------------------------------------------------------------------------------- /docs/assets/img/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/assets/img/npm.svg: -------------------------------------------------------------------------------- 1 | NPM icon -------------------------------------------------------------------------------- /docs/assets/img/twitter.svg: -------------------------------------------------------------------------------- 1 | Twitter icon -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 14 | 15 | docsify-tabs - A docsify.js plugin for rendering tabbed content from markdown 16 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 44 | 45 | 46 | 47 |
48 | 49 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # docsify-tabs 2 | 3 | [![NPM](https://img.shields.io/npm/v/docsify-tabs.svg?style=flat-square)](https://www.npmjs.com/package/docsify-tabs) 4 | [![GitHub Workflow Status (master)](https://img.shields.io/github/actions/workflow/status/jhildenbiddle/docsify-tabs/test.yml?branch=master&label=checks&style=flat-square)](https://github.com/jhildenbiddle/docsify-tabs/actions?query=branch%3Amaster+) 5 | [![Codacy grade](https://img.shields.io/codacy/grade/e9c2a9504211450ab39e0d72a1158a47.svg?style=flat-square)](https://app.codacy.com/gh/jhildenbiddle/docsify-tabs/dashboard) 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://github.com/jhildenbiddle/docsify-tabs/blob/master/LICENSE) 7 | [![jsDelivr](https://data.jsdelivr.com/v1/package/npm/docsify-tabs/badge)](https://www.jsdelivr.com/package/npm/docsify-tabs) 8 | [![Sponsor this project](https://img.shields.io/static/v1?style=flat-square&label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/jhildenbiddle) 9 | [![Add a star on GitHub](https://img.shields.io/github/stars/jhildenbiddle/docsify-tabs?style=social)](https://github.com/jhildenbiddle/docsify-tabs) 10 | [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?url=https%3A%2F%2Fgithub.com%2Fjhildenbiddle%2Fdocsify-tabs&hashtags=docsify,developers,frontend,plugin) 11 | 12 | A [docsify.js](https://docsify.js.org) plugin for rendering tabbed content from markdown. 13 | 14 | ## Demo 15 | 16 | A basic tab set using the default [options](#options). 17 | 18 | 19 | 20 | #### **Tab A** 21 | 22 | ### Heading for Tab A {docsify-ignore} 23 | 24 | This is some text. 25 | 26 | - List item A-1 27 | - List item A-2 28 | 29 | ```js 30 | // JavaScript 31 | function add(a, b) { 32 | return a + b; 33 | } 34 | ``` 35 | 36 | 37 | 38 | #### **Nested Tab 1** 39 | 40 | Life is what happens when you're busy making other plans. 41 | 42 | \- _John Lennon_ 43 | 44 | #### **Nested Tab 2** 45 | 46 | The greatest glory in living lies not in never falling, but in rising every time we fall. 47 | 48 | \- _Nelson Mandela_ 49 | 50 | 51 | 52 | #### **Tab B** 53 | 54 | ### Heading for Tab B {docsify-ignore} 55 | 56 | This is some text. 57 | 58 | - List item B-1 59 | - List item B-2 60 | 61 | ```css 62 | /* CSS */ 63 | body { 64 | background: white; 65 | color: #222; 66 | } 67 | ``` 68 | 69 | #### **Tab C** 70 | 71 | This is some text. 72 | 73 | - List item C-1 74 | - List item C-2 75 | 76 | ```html 77 | 78 |

Heading

79 |

This is a paragraph.

80 | ``` 81 | 82 | 83 | 84 | ?> Like this plugin? Check out [docsify-themeable](https://jhildenbiddle.github.io/docsify-themeable) for your site theme, [docsify-plugin-ethicalads](https://jhildenbiddle.github.io/docsify-plugin-ethicalads/) for EthicalAds integration, and [docsify-plugin-runkit](https://jhildenbiddle.github.io/docsify-plugin-runkit/) for live JavaScript REPLs! 85 | 86 | ## Features 87 | 88 | - Generate tabbed content using unobtrusive markup 89 | - Persist tab selections on refresh/revisit 90 | - Sync tab selection for tabs with matching labels 91 | - Nest tab sets within tab sets 92 | - Style tabs using "classic" or "material" tab theme 93 | - Customize styles without complex CSS using CSS custom properties 94 | - Compatible with [docsify-themeable](https://jhildenbiddle.github.io/docsify-themeable/) themes 95 | 96 | **Limitations** 97 | 98 | - Tabs wraps when their combined width exceeds the content area width 99 | 100 | ## Installation 101 | 102 | 1. Add the docsify-tabs plugin to your `index.html` after docsify. The plugin is available on [jsdelivr](https://www.jsdelivr.com/package/npm/docsify-tabs) (below), [unpkg](https://unpkg.com/browse/docsify-tabs/), and other CDN services that auto-publish npm packages. 103 | 104 | ```html 105 | 106 | 107 | 108 | 109 | 110 | ``` 111 | 112 | !> Note the `@` version number lock in the URLs above. This prevents breaking changes in future releases from affecting your project and is therefore the safest method of loading dependencies from a CDN. When a new major version is released, you will need to manually update your CDN URLs by changing the version number after the @ symbol. 113 | 114 | 1. Review the [Options](#options) section and configure as needed. 115 | 116 | ```javascript 117 | window.$docsify = { 118 | // ... 119 | tabs: { 120 | persist: true, // default 121 | sync: true, // default 122 | theme: 'classic', // default 123 | tabComments: true, // default 124 | tabHeadings: true // default 125 | } 126 | }; 127 | ``` 128 | 129 | 1. Review the [Customization](#customization) section and set theme properties as needed. 130 | 131 | ```html 132 | 138 | ``` 139 | 140 | ## Usage 141 | 142 | 1. Define a tab set using `tabs:start` and `tabs:end` HTML comments. 143 | 144 | HTML comments are used to mark the start and end of a tab set. The use of HTML comments prevents tab-related markup from being displayed when markdown is rendered as HTML outside of your docsify site (e.g. GitHub, GitLab, etc). 145 | 146 | ```markdown 147 | 148 | 149 | ... 150 | 151 | 152 | ``` 153 | 154 | 1. Define tabs within a tab set using heading + bold markdown. 155 | 156 | Heading text will be used as the tab label, and all proceeding content will be associated with that tab up to start of the next tab or a `tab:end` comment. The use of heading + bold markdown allows tabs to be defined using standard markdown and ensures that tab content is displayed with a heading when rendered outside of your docsify site (e.g. GitHub, GitLab, etc). 157 | 158 | ```markdown 159 | 160 | 161 | #### **English** 162 | 163 | Hello! 164 | 165 | #### **French** 166 | 167 | Bonjour! 168 | 169 | #### **Italian** 170 | 171 | Ciao! 172 | 173 | 174 | ``` 175 | 176 | See [`options.tabHeadings`](#tabheadings) for details or [`options.tabComments`](#tabcomments) for an alternate method of defining tabs using HTML comments. 177 | 178 | 1. Voilà! A tab set is formed. 179 | 180 | 181 | 182 | #### **English** 183 | 184 | Hello! 185 | 186 | #### **French** 187 | 188 | Bonjour! 189 | 190 | #### **Italian** 191 | 192 | Ciao! 193 | 194 | 195 | 196 | ## Options 197 | 198 | Options are set within the [`window.$docsify`](https://docsify.js.org/#/configuration) configuration under the `tabs` key: 199 | 200 | ```html 201 | 213 | ``` 214 | 215 | ### persist 216 | 217 | - Type: `boolean` 218 | - Default: `true` 219 | 220 | Determines if tab selections will be restored after a page refresh/revisit. 221 | 222 | **Configuration** 223 | 224 | ```javascript 225 | window.$docsify = { 226 | // ... 227 | tabs: { 228 | persist: true // default 229 | } 230 | }; 231 | ``` 232 | 233 | ### sync 234 | 235 | - Type: `boolean` 236 | - Default: `true` 237 | 238 | Determines if tab selections will be synced across tabs with matching labels. 239 | 240 | **Configuration** 241 | 242 | ```javascript 243 | window.$docsify = { 244 | // ... 245 | tabs: { 246 | sync: true // default 247 | } 248 | }; 249 | ``` 250 | 251 | **Demo** 252 | 253 | 254 | 255 | #### **macOS** 256 | 257 | Instructions for macOS... 258 | 259 | #### **Windows** 260 | 261 | Instructions for Windows... 262 | 263 | #### **Linux** 264 | 265 | Instructions for Linux... 266 | 267 | 268 | 269 | 270 | 271 | #### **macOS** 272 | 273 | More instructions for macOS... 274 | 275 | #### **Windows** 276 | 277 | More instructions for Windows... 278 | 279 | #### **Linux** 280 | 281 | More instructions for Linux... 282 | 283 | 284 | 285 | ### theme 286 | 287 | - Type: `string|boolean` 288 | - Accepts: `'classic'`, `'material'`, `false` 289 | - Default: `'classic'` 290 | 291 | Sets the tab theme. A value of `false` will indicate that no theme should be applied, which should be used when creating custom tab themes. 292 | 293 | **Configuration** 294 | 295 | ```javascript 296 | window.$docsify = { 297 | // ... 298 | tabs: { 299 | theme: 'classic' // default 300 | } 301 | }; 302 | ``` 303 | 304 | **Demo** 305 | 306 | 309 | 312 | 315 | 316 | 317 | 318 | #### **Tab A** 319 | 320 | This is some text. 321 | 322 | #### **Tab B** 323 | 324 | This is some more text. 325 | 326 | #### **Tab C** 327 | 328 | Yes, this is even more text. 329 | 330 | 331 | 332 | ### tabComments 333 | 334 | - Type: `boolean` 335 | - Default: `true` 336 | 337 | Determines if tabs within a tab set can be defined using tab comments. 338 | 339 | Note that defining tabs using HTML comments means tab content will not be labeled when rendered outside of your docsify site (e.g. GitHub, GitLab, etc). For this reason, defining tabs using [`options.tabHeadings`](#tabheadings) is recommended. 340 | 341 | **Configuration** 342 | 343 | ```javascript 344 | window.$docsify = { 345 | // ... 346 | tabs: { 347 | tabComments: true // default 348 | } 349 | }; 350 | ``` 351 | 352 | **Example** 353 | 354 | ```markdown 355 | 356 | 357 | 358 | 359 | Hello! 360 | 361 | 362 | 363 | Bonjour! 364 | 365 | 366 | 367 | Ciao! 368 | 369 | 370 | ``` 371 | 372 | ### tabHeadings 373 | 374 | - Type: `boolean` 375 | - Default: `true` 376 | 377 | Determines if tabs within a tab set can be defined using heading + bold markdown. 378 | 379 | The use of heading + bold markdown allows tabs to be defined using standard markdown and ensures that tab content is displayed with a heading when rendered outside of your docsify site (e.g. GitHub, GitLab, etc). Heading levels 1-6 are supported (e.g. `#` - `######`) as are both asterisks (`**`) and underscores (`__`) for bold text via markdown. 380 | 381 | **Configuration** 382 | 383 | ```javascript 384 | window.$docsify = { 385 | // ... 386 | tabs: { 387 | tabHeadings: true // default 388 | } 389 | }; 390 | ``` 391 | 392 | **Example** 393 | 394 | ```markdown 395 | 396 | 397 | #### **English** 398 | 399 | Hello! 400 | 401 | #### **French** 402 | 403 | Bonjour! 404 | 405 | #### **Italian** 406 | 407 | Ciao! 408 | 409 | 410 | ``` 411 | 412 | ## Customization 413 | 414 | ### Themes 415 | 416 | See [`options.theme`](#theme) for details on available themes. 417 | 418 | ### Theme Properties 419 | 420 | Theme properties allow you to customize tab styles without writing complex CSS. The following list contains the default theme values. 421 | 422 | [vars.css](https://raw.githubusercontent.com/jhildenbiddle/docsify-tabs/master/src/css/vars.css ':include :type:code') 423 | 424 | To set theme properties, add a ` 433 | ``` 434 | 435 | ### Custom Styles 436 | 437 | The easiest way to create custom tab styles is by using markdown and/or HTML in your tab label. 438 | 439 | 440 | 441 | #### **Bold** 442 | 443 | **Tab Markdown** 444 | 445 | ```markdown 446 | 447 | 448 | #### **Bold** 449 | 450 | ... 451 | 452 | 453 | ``` 454 | 455 | #### **Italic** 456 | 457 | **Tab Markdown** 458 | 459 | ```markdown 460 | 461 | 462 | #### **Italic** 463 | 464 | ... 465 | 466 | 467 | ``` 468 | 469 | #### **Red** 470 | 471 | **Tab Markdown** 472 | 473 | ```markdown 474 | 475 | 476 | #### **Red** 477 | 478 | ... 479 | 480 | 481 | ``` 482 | 483 | #### **:smile:** 484 | 485 | **Tab Markdown** 486 | 487 | ```markdown 488 | 489 | 490 | #### **:smile:** 491 | 492 | ... 493 | 494 | 495 | ``` 496 | 497 | #### **😀** 498 | 499 | **Tab Markdown** 500 | 501 | ```markdown 502 | 503 | 504 | #### **😀** 505 | 506 | ... 507 | 508 | 509 | ``` 510 | 511 | #### **Badge New!** 512 | 513 | **Tab Markdown** 514 | 515 | ```markdown 516 | 517 | 518 | #### **Badge New!** 519 | 520 | ... 521 | 522 | 523 | ``` 524 | 525 | #### CSS 526 | 527 | ```html 528 | 543 | ``` 544 | 545 | 546 | 547 | More advanced styling can be applied by leveraging the CSS class names and data attributes associated with tab containers, toggles, labels, and content blocks. Consider the following tab markdown and the HTML output generated by docsify-tabs: 548 | 549 | ```markdown 550 | 551 | 552 | #### **My Tab** 553 | 554 | ... 555 | 556 | 557 | ``` 558 | 559 | ```html 560 | 561 |
...
562 | ``` 563 | 564 | When the tab is active, note the addition of the `docsify-tabs__tab--active` class: 565 | 566 | ```html 567 | 568 | ``` 569 | 570 | **Examples** 571 | 572 | 573 | 574 | #### **Active State** 575 | 576 | **Markdown** 577 | 578 | ```markdown 579 | 580 | 581 | #### **Active State** 582 | 583 | ... 584 | 585 | 586 | ``` 587 | 588 | **HTML Output** 589 | 590 | ```html 591 | 594 |
...
595 | ``` 596 | 597 | **Custom CSS** 598 | 599 | ```css 600 | .docsify-tabs__tab--active[data-tab='active state'] { 601 | box-shadow: none; 602 | background: #13547a; 603 | color: white; 604 | } 605 | .docsify-tabs__content[data-tab-content='active state'] { 606 | background-image: linear-gradient(0deg, #80d0c7 0%, #13547a 100%); 607 | } 608 | .docsify-tabs__content[data-tab-content='active state'] p strong { 609 | color: white; 610 | } 611 | ``` 612 | 613 | #### **CodePen** 614 | 615 |
616 | 621 |
622 | 623 | **Markdown** 624 | 625 | ```markdown 626 | 627 | 628 | #### **CodePen** 629 | 630 | CodePen Embed Code... 631 | 632 | 633 | ``` 634 | 635 | **HTML Output** 636 | 637 | ```html 638 | 639 |
...
640 | ``` 641 | 642 | **Custom CSS** 643 | 644 | ```css 645 | [data-tab-content='codepen'] .cp_embed_wrapper { 646 | position: relative; 647 | top: calc(0px - var(--docsifytabs-content-padding)); 648 | left: calc(0px - var(--docsifytabs-content-padding)); 649 | width: calc(100% + calc(var(--docsifytabs-content-padding) * 2)); 650 | margin-bottom: calc(0px - var(--docsifytabs-content-padding)); 651 | } 652 | 653 | [data-tab-content='codepen'] .cp_embed_wrapper > * { 654 | margin: 0; 655 | } 656 | ``` 657 | 658 | #### **Badge** 659 | 660 | **Markdown** 661 | 662 | ```markdown 663 | 664 | 665 | #### **Badge** 666 | 667 | ... 668 | 669 | 670 | ``` 671 | 672 | **Custom CSS** 673 | 674 | ```css 675 | [data-tab='badge']:after { 676 | content: 'New!'; 677 | position: absolute; 678 | top: 0; 679 | right: 0; 680 | transform: translate(35%, -45%); 681 | padding: 0.25em 0.35em; 682 | border-radius: 3px; 683 | background: red; 684 | color: white; 685 | font-family: sans-serif; 686 | font-size: 11px; 687 | font-weight: bold; 688 | } 689 | ``` 690 | 691 | 692 | 693 | ## Sponsorship 694 | 695 | A [sponsorship](https://github.com/sponsors/jhildenbiddle) is more than just a way to show appreciation for the open-source authors and projects we rely on; it can be the spark that ignites the next big idea, the inspiration to create something new, and the motivation to share so that others may benefit. 696 | 697 | If you benefit from this project, please consider lending your support and encouraging future efforts by [becoming a sponsor](https://github.com/sponsors/jhildenbiddle). 698 | 699 | Thank you! 🙏🏻 700 | 701 | 702 | 703 | ## Contact & Support 704 | 705 | - Follow 👨🏻‍💻 **@jhildenbiddle** on [Twitter](https://twitter.com/jhildenbiddle) and [GitHub](https://github.com/jhildenbiddle) for announcements 706 | - Create a 💬 [GitHub issue](https://github.com/jhildenbiddle/docsify-tabs/issues) for bug reports, feature requests, or questions 707 | - Add a ⭐️ [star on GitHub](https://github.com/jhildenbiddle/docsify-tabs) and 🐦 [tweet](https://twitter.com/intent/tweet?url=https%3A%2F%2Fgithub.com%2Fjhildenbiddle%2Fdocsify-tabs&hashtags=css,developers,frontend,javascript) to promote the project 708 | - Become a 💖 [sponsor](https://github.com/sponsors/jhildenbiddle) to support the project and future efforts 709 | 710 | ## License 711 | 712 | This project is licensed under the [MIT license](https://github.com/jhildenbiddle/docsify-tabs/blob/master/LICENSE). 713 | 714 | Copyright (c) John Hildenbiddle ([@jhildenbiddle](https://twitter.com/jhildenbiddle)) 715 | -------------------------------------------------------------------------------- /docs/sidebar.md: -------------------------------------------------------------------------------- 1 | - [Documentation](/) 2 | - [Changelog](changelog) 3 | - **Links** 4 | - [![GitHub](assets/img/github.svg)GitHub](https://github.com/jhildenbiddle/docsify-tabs) 5 | - [![NPM](assets/img/npm.svg)NPM](https://www.npmjs.com/package/docsify-tabs) 6 | - [![Twitter](assets/img/twitter.svg)@jhildenbiddle](http://twitter.com/jhildenbiddle) 7 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import eslintConfigPrettier from 'eslint-config-prettier'; 2 | import globals from 'globals'; 3 | import js from '@eslint/js'; 4 | 5 | export default [ 6 | { 7 | ignores: ['dist'] 8 | }, 9 | js.configs.recommended, 10 | eslintConfigPrettier, 11 | { 12 | languageOptions: { 13 | globals: { 14 | ...globals.browser 15 | } 16 | }, 17 | rules: { 18 | 'array-bracket-spacing': ['error', 'never'], 19 | 'array-callback-return': ['error'], 20 | 'block-scoped-var': ['error'], 21 | 'block-spacing': ['error', 'always'], 22 | curly: ['error'], 23 | 'dot-notation': ['error'], 24 | eqeqeq: ['error'], 25 | 'no-console': ['warn'], 26 | 'no-floating-decimal': ['error'], 27 | 'no-implicit-coercion': ['error'], 28 | 'no-implicit-globals': ['error'], 29 | 'no-loop-func': ['error'], 30 | 'no-return-assign': ['error'], 31 | 'no-template-curly-in-string': ['error'], 32 | 'no-unneeded-ternary': ['error'], 33 | 'no-unused-vars': ['error', { args: 'none' }], 34 | 'no-useless-computed-key': ['error'], 35 | 'no-useless-return': ['error'], 36 | 'no-var': ['error'], 37 | 'prefer-const': ['error'], 38 | quotes: ['error', 'single'], 39 | semi: ['error', 'always'] 40 | } 41 | } 42 | ]; 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docsify-tabs", 3 | "version": "1.6.3", 4 | "description": "A docsify.js plugin for rendering tabbed content from markdown", 5 | "author": "John Hildenbiddle", 6 | "license": "MIT", 7 | "homepage": "https://jhildenbiddle.github.io/docsify-tabs/", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://jhildenbiddle@github.com/jhildenbiddle/docsify-tabs.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/jhildenbiddle/docsify-tabs/issues" 14 | }, 15 | "keywords": [ 16 | "docs", 17 | "docsify", 18 | "docsify.js", 19 | "documentation", 20 | "generator", 21 | "javascript", 22 | "js", 23 | "markdown", 24 | "md", 25 | "plugin", 26 | "tab", 27 | "tabs" 28 | ], 29 | "browserslist": [ 30 | "ie >= 11" 31 | ], 32 | "files": [ 33 | "dist" 34 | ], 35 | "main": "dist/docsify-tabs.js", 36 | "unpkg": "dist/docsify-tabs.min.js", 37 | "type": "module", 38 | "scripts": { 39 | "build": "rollup -c", 40 | "clean": "rimraf --glob dist/*", 41 | "escheck": "es-check es5 'dist/**/*.js'", 42 | "lint": "prettier . --check && eslint . && markdownlint *.md docs/*.md --ignore node_modules", 43 | "lint:fix": "prettier . --write && eslint . --fix", 44 | "prepare": "run-s clean build", 45 | "serve": "node server.js", 46 | "start": "run-p watch serve", 47 | "test": "echo \"Error: no test specified\" && exit 1", 48 | "watch": "run-p 'build -- -w'", 49 | "version": "run-s prepare lint escheck" 50 | }, 51 | "devDependencies": { 52 | "@babel/core": "^7.13.16", 53 | "@babel/preset-env": "^7.13.15", 54 | "@eslint/js": "^9.1.1", 55 | "@rollup/plugin-babel": "^6.0.4", 56 | "@rollup/plugin-commonjs": "^25.0.7", 57 | "@rollup/plugin-json": "^6.1.0", 58 | "@rollup/plugin-node-resolve": "^15.2.3", 59 | "@rollup/plugin-terser": "^0.4.4", 60 | "autoprefixer": "^10.2.5", 61 | "browser-sync": "^3.0.2", 62 | "compression": "^1.7.4", 63 | "es-check": "^7.0.0", 64 | "eslint": "^9.1.1", 65 | "eslint-config-prettier": "^9.1.0", 66 | "globals": "^15.0.0", 67 | "markdownlint-cli": "^0.39.0", 68 | "mergician": "^2.0.0", 69 | "npm-run-all": "^4.1.5", 70 | "postcss": "^8.3.6", 71 | "postcss-custom-properties": "^13.3.5", 72 | "postcss-flexbugs-fixes": "^5.0.2", 73 | "postcss-import": "^16.0.1", 74 | "prettier": "^3.2.5", 75 | "rimraf": "^5.0.5", 76 | "rollup": "^4.12.0", 77 | "rollup-plugin-postcss": "^4.0.1", 78 | "sass": "^1.49.11" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | map: false, 3 | plugins: [ 4 | require('postcss-import')(), 5 | require('autoprefixer')(), 6 | require('postcss-custom-properties')(), 7 | require('postcss-flexbugs-fixes')() 8 | ] 9 | }; 10 | -------------------------------------------------------------------------------- /prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { babel } from '@rollup/plugin-babel'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import fs from 'node:fs'; 4 | import json from '@rollup/plugin-json'; 5 | import { mergician } from 'mergician'; 6 | import path from 'node:path'; 7 | import postcss from 'rollup-plugin-postcss'; 8 | import nodeResolve from '@rollup/plugin-node-resolve'; 9 | import terser from '@rollup/plugin-terser'; 10 | 11 | const pkg = JSON.parse( 12 | fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8') // prettier-ignore 13 | ); 14 | 15 | // Settings 16 | // ============================================================================= 17 | // Copyright 18 | const currentYear = new Date().getFullYear(); 19 | const releaseYear = 2018; 20 | 21 | // Output 22 | const entryFile = path.resolve('.', 'src', 'js', 'index.js'); 23 | const outputFile = path.resolve('.', 'dist', `${pkg.name}.js`); 24 | 25 | // Banner 26 | const bannerData = [ 27 | `${pkg.name}`, 28 | `v${pkg.version}`, 29 | `${pkg.homepage}`, 30 | `(c) ${releaseYear}${currentYear === releaseYear ? '' : '-' + currentYear} ${pkg.author}`, 31 | `${pkg.license} license` 32 | ]; 33 | 34 | // Plugins 35 | const pluginSettings = { 36 | babel: { 37 | babelrc: false, 38 | exclude: ['node_modules/**'], 39 | babelHelpers: 'bundled', 40 | presets: [ 41 | [ 42 | '@babel/preset-env', 43 | { 44 | modules: false, 45 | targets: { 46 | browsers: ['ie >= 11'] 47 | } 48 | } 49 | ] 50 | ] 51 | }, 52 | postcss: { 53 | inject: { 54 | insertAt: 'top' 55 | }, 56 | minimize: true 57 | }, 58 | terser: { 59 | beautify: { 60 | compress: false, 61 | mangle: false, 62 | output: { 63 | beautify: true, 64 | comments: /(?:^!|@(?:license|preserve))/ 65 | } 66 | }, 67 | minify: { 68 | compress: true, 69 | mangle: true, 70 | output: { 71 | comments: new RegExp(pkg.name) 72 | } 73 | } 74 | } 75 | }; 76 | 77 | // Config 78 | // ============================================================================= 79 | // Base 80 | const config = { 81 | input: entryFile, 82 | output: { 83 | banner: `/*!\n * ${bannerData.join('\n * ')}\n */`, 84 | file: outputFile, 85 | sourcemap: true 86 | }, 87 | plugins: [ 88 | nodeResolve(), 89 | commonjs(), 90 | json(), 91 | postcss(pluginSettings.postcss), 92 | babel(pluginSettings.babel) 93 | ], 94 | watch: { 95 | clearScreen: false 96 | } 97 | }; 98 | 99 | // Formats 100 | // ----------------------------------------------------------------------------- 101 | // IIFE 102 | const iife = mergician({}, config, { 103 | output: { 104 | format: 'iife' 105 | }, 106 | plugins: config.plugins.concat([terser(pluginSettings.terser.beautify)]) 107 | }); 108 | 109 | // IIFE (Minified) 110 | const iifeMinified = mergician({}, config, { 111 | output: { 112 | file: iife.output.file.replace(/\.js$/, '.min.js'), 113 | format: iife.output.format 114 | }, 115 | plugins: config.plugins.concat([terser(pluginSettings.terser.minify)]) 116 | }); 117 | 118 | // Exports 119 | // ============================================================================= 120 | export default [iife, iifeMinified]; 121 | -------------------------------------------------------------------------------- /screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhildenbiddle/docsify-tabs/ecc0184c042627961cb54882f1a43adecff0577e/screenshot.jpg -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import { create } from 'browser-sync'; 2 | import compression from 'compression'; 3 | 4 | const bsServer = create(); 5 | 6 | bsServer.init({ 7 | files: ['./dist/**/*.*', './docs/**/*.*'], 8 | ghostMode: { 9 | clicks: false, 10 | forms: false, 11 | scroll: false 12 | }, 13 | open: false, 14 | notify: false, 15 | cors: true, 16 | reloadDebounce: 1000, 17 | reloadOnRestart: true, 18 | server: { 19 | baseDir: ['./docs/'], 20 | middleware: [compression()], 21 | routes: { 22 | '/CHANGELOG.md': './CHANGELOG.md' 23 | } 24 | }, 25 | serveStatic: ['./dist/'], 26 | rewriteRules: [ 27 | // Replace CDN URLs with local paths 28 | { 29 | match: /https?.*\/CHANGELOG.md/g, 30 | replace: '/CHANGELOG.md' 31 | }, 32 | { 33 | // CDN versioned default 34 | // Ex1: //cdn.com/package-name 35 | // Ex2: http://cdn.com/package-name@1.0.0 36 | // Ex3: https://cdn.com/package-name@latest 37 | match: /(?:https?:)*\/\/.*cdn.*docsify-tabs[@\d.latest]*(?=["'])/g, 38 | replace: '/docsify-tabs.js' 39 | }, 40 | { 41 | // CDN paths to local paths 42 | // Ex1: //cdn.com/package-name/path/file.js => /path/file.js 43 | // Ex2: http://cdn.com/package-name@1.0.0/dist/file.js => /dist/file.js 44 | // Ex3: https://cdn.com/package-name@latest/dist/file.js => /dist/file.js 45 | match: /(?:https?:)*\/\/.*cdn.*docsify-tabs[@\d.latest]*\/(?:dist\/)/g, 46 | replace: '/' 47 | } 48 | ] 49 | }); 50 | -------------------------------------------------------------------------------- /src/css/vars.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* Tab blocks */ 3 | --docsifytabs-border-color: #ededed; 4 | --docsifytabs-border-px: 1px; 5 | --docsifytabs-border-radius-px: ; 6 | --docsifytabs-margin: 1.5em 0; 7 | 8 | /* Tabs */ 9 | --docsifytabs-tab-background: #f8f8f8; 10 | --docsifytabs-tab-background--active: var(--docsifytabs-content-background); 11 | --docsifytabs-tab-color: #999; 12 | --docsifytabs-tab-color--active: inherit; 13 | --docsifytabs-tab-highlight-px: 3px; 14 | --docsifytabs-tab-highlight-color: var(--theme-color, currentColor); 15 | --docsifytabs-tab-padding: 0.6em 1em; 16 | 17 | /* Tab content */ 18 | --docsifytabs-content-background: inherit; 19 | --docsifytabs-content-padding: 1.5rem; 20 | } 21 | -------------------------------------------------------------------------------- /src/js/index.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | // ============================================================================= 3 | import { version as pkgVersion } from '../../package.json'; 4 | import '../scss/style.scss'; 5 | 6 | // Constants and variables 7 | // ============================================================================= 8 | const commentReplaceMark = 'tabs:replace'; 9 | const classNames = { 10 | tabsContainer: 'content', 11 | tabBlock: 'docsify-tabs', 12 | tabButton: 'docsify-tabs__tab', 13 | tabButtonActive: 'docsify-tabs__tab--active', 14 | tabContent: 'docsify-tabs__content' 15 | }; 16 | const regex = { 17 | // Matches markdown code blocks (inline and multi-line) 18 | // Example: ```text``` 19 | codeMarkup: /(```[\s\S]*?```)/gm, 20 | 21 | // Matches tab replacement comment 22 | // 0: Match 23 | // 1: Replacement HTML 24 | commentReplaceMarkup: new RegExp(``), 25 | 26 | // Matches inner-most tab set by start/end comment 27 | // Ex: () 28 | // 0: Match 29 | // 1: Indent 30 | // 2: Start comment: 31 | // 3: undefined 32 | // 4: End comment: 33 | tabBlockMarkup: 34 | /( *)()(?:(?!())[\s\S])*()/, 35 | 36 | // Matches tab label and content 37 | // 0: Match 38 | // 1: Label: 39 | // 2: Content 40 | tabCommentMarkup: 41 | /[\r\n]*(\s*)[\r\n]+([\s\S]*?)[\r\n]*\s*(?=)/m 49 | }; 50 | const settings = { 51 | persist: true, 52 | sync: true, 53 | theme: 'classic', 54 | tabComments: true, 55 | tabHeadings: true 56 | }; 57 | 58 | const storageKeys = { 59 | get persist() { 60 | return `docsify-tabs.persist.${window.location.pathname}`; 61 | }, 62 | sync: 'docsify-tabs.sync' 63 | }; 64 | 65 | // Functions 66 | // ============================================================================= 67 | /** 68 | * Traverses the element and its parents until it finds a node that matches the 69 | * provided selector string. Will return itself or the matching ancestor. 70 | * 71 | * @param {object} elm 72 | * @param {string} closestSelectorString 73 | * @return {(object|null)} 74 | */ 75 | function getClosest(elm, closestSelectorString) { 76 | if (Element.prototype.closest) { 77 | return elm.closest(closestSelectorString); 78 | } 79 | 80 | while (elm) { 81 | const isMatch = matchSelector(elm, closestSelectorString); 82 | 83 | if (isMatch) { 84 | return elm; 85 | } 86 | 87 | elm = elm.parentNode || null; 88 | } 89 | 90 | return elm; 91 | } 92 | 93 | /** 94 | * Checks to see if the element would be selected by the provided selectorString 95 | * 96 | * @param {object} elm 97 | * @param {string} selectorString 98 | * @return {boolean} 99 | */ 100 | function matchSelector(elm, selectorString) { 101 | const matches = 102 | Element.prototype.matches || 103 | Element.prototype.msMatchesSelector || 104 | Element.prototype.webkitMatchesSelector; 105 | 106 | return matches.call(elm, selectorString); 107 | } 108 | 109 | /** 110 | * Converts tab content into "stage 1" markup. Stage 1 markup contains temporary 111 | * comments which are replaced with HTML during Stage 2. This approach allows 112 | * all markdown to be converted to HTML before tab-specific HTML is added. 113 | * 114 | * @param {string} content 115 | * @returns {string} 116 | */ 117 | function renderTabsStage1(content, vm) { 118 | const codeBlockMatch = content.match(regex.codeMarkup) || []; 119 | const codeBlockMarkers = codeBlockMatch.map((item, i) => { 120 | const codeMarker = ``; 121 | 122 | // Replace code block with marker to ensure tab markup within code 123 | // blocks is not processed. These markers are replaced with their 124 | // associated code blocs after tabs have been processed. 125 | content = content.replace(item, () => codeMarker); 126 | 127 | return codeMarker; 128 | }); 129 | const tabTheme = settings.theme ? `${classNames.tabBlock}--${settings.theme}` : ''; 130 | const tempElm = document.createElement('div'); 131 | 132 | let tabBlockMatch = content.match(regex.tabBlockMarkup); 133 | let tabIndex = 1; 134 | 135 | // Process each tab set 136 | while (tabBlockMatch) { 137 | let tabBlockOut = tabBlockMatch[0]; 138 | 139 | const tabBlockIndent = tabBlockMatch[1]; 140 | const tabBlockStart = tabBlockMatch[2]; 141 | const tabBlockEnd = tabBlockMatch[4]; 142 | const hasTabComments = settings.tabComments && regex.tabCommentMarkup.test(tabBlockOut); 143 | const hasTabHeadings = settings.tabHeadings && regex.tabHeadingMarkup.test(tabBlockOut); 144 | 145 | let tabMatch; 146 | let tabStartReplacement = ''; 147 | let tabEndReplacement = ''; 148 | 149 | if (hasTabComments || hasTabHeadings) { 150 | tabStartReplacement = ``; 151 | tabEndReplacement = `\n${tabBlockIndent}`; 152 | 153 | // Process each tab panel 154 | while ( 155 | (tabMatch = 156 | (settings.tabComments ? regex.tabCommentMarkup.exec(tabBlockOut) : null) || 157 | (settings.tabHeadings ? regex.tabHeadingMarkup.exec(tabBlockOut) : null)) !== null 158 | ) { 159 | // Process tab title as markdown 160 | // Ex: 161 | tempElm.innerHTML = tabMatch[2].trim() 162 | ? vm.compiler.compile(tabMatch[2]).replace(/<\/?p>/g, '') 163 | : `Tab ${tabIndex}`; 164 | 165 | const tabTitle = tempElm.innerHTML; 166 | const tabContent = (tabMatch[3] || '').trim(); 167 | const tabData = ( 168 | tempElm.textContent || 169 | tempElm.firstChild.getAttribute('alt') || 170 | tempElm.firstChild.getAttribute('src') 171 | ) 172 | .trim() 173 | .toLowerCase(); 174 | 175 | // Use replace function to avoid regex special replacement 176 | // strings being processed ($$, $&, $`, $', $n) 177 | tabBlockOut = tabBlockOut.replace(tabMatch[0], () => 178 | [ 179 | `\n${tabBlockIndent}`, 180 | `\n${tabBlockIndent}`, 181 | `\n\n${tabBlockIndent}${tabContent}`, 182 | `\n\n${tabBlockIndent}` 183 | ].join('') 184 | ); 185 | 186 | tabIndex++; 187 | } 188 | } 189 | 190 | tabBlockOut = tabBlockOut.replace(tabBlockStart, () => tabStartReplacement); 191 | tabBlockOut = tabBlockOut.replace(tabBlockEnd, () => tabEndReplacement); 192 | content = content.replace(tabBlockMatch[0], () => tabBlockOut); 193 | 194 | tabBlockMatch = content.match(regex.tabBlockMarkup); 195 | } 196 | 197 | // Restore code blocks 198 | codeBlockMarkers.forEach((item, i) => { 199 | content = content.replace(item, () => codeBlockMatch[i]); 200 | }); 201 | 202 | return content; 203 | } 204 | 205 | /** 206 | * Converts "stage 1" markup into final markup by replacing temporary comments 207 | * with HTML. 208 | * 209 | * @param {string} html 210 | * @returns {string} 211 | */ 212 | function renderTabsStage2(html) { 213 | let tabReplaceMatch; 214 | 215 | while ((tabReplaceMatch = regex.commentReplaceMarkup.exec(html)) !== null) { 216 | const tabComment = tabReplaceMatch[0]; 217 | const tabReplacement = tabReplaceMatch[1] || ''; 218 | 219 | html = html.replace(tabComment, () => tabReplacement); 220 | } 221 | 222 | return html; 223 | } 224 | 225 | /** 226 | * Sets the initial active tab for each tab group: the tab containing the 227 | * matching element ID from the URL, the first tab in the group, or the last tab 228 | * clicked (if persist option is enabled). 229 | */ 230 | function setDefaultTabs() { 231 | const tabsContainer = document.querySelector(`.${classNames.tabsContainer}`); 232 | const tabBlocks = tabsContainer 233 | ? Array.apply(null, tabsContainer.querySelectorAll(`.${classNames.tabBlock}`)) 234 | : []; 235 | const tabStoragePersist = JSON.parse(sessionStorage.getItem(storageKeys.persist)) || {}; 236 | const tabStorageSync = JSON.parse(sessionStorage.getItem(storageKeys.sync)) || []; 237 | 238 | setActiveTabFromAnchor(); 239 | 240 | tabBlocks.forEach((tabBlock, index) => { 241 | let activeButton = Array.apply(null, tabBlock.children).filter(elm => 242 | matchSelector(elm, `.${classNames.tabButtonActive}`) 243 | )[0]; 244 | 245 | if (!activeButton) { 246 | if (settings.sync && tabStorageSync.length) { 247 | activeButton = tabStorageSync 248 | .map( 249 | label => 250 | Array.apply(null, tabBlock.children).filter(elm => 251 | matchSelector(elm, `.${classNames.tabButton}[data-tab="${label}"]`) 252 | )[0] 253 | ) 254 | .filter(elm => elm)[0]; 255 | } 256 | 257 | if (!activeButton && settings.persist) { 258 | activeButton = Array.apply(null, tabBlock.children).filter(elm => 259 | matchSelector(elm, `.${classNames.tabButton}[data-tab="${tabStoragePersist[index]}"]`) 260 | )[0]; 261 | } 262 | 263 | activeButton = activeButton || tabBlock.querySelector(`.${classNames.tabButton}`); 264 | activeButton && activeButton.classList.add(classNames.tabButtonActive); 265 | } 266 | }); 267 | } 268 | 269 | /** 270 | * Sets the active tab within a group. Optionally stores the selection so it can 271 | * persist across page loads and syncs active state to tabs with same data attr. 272 | * 273 | * @param {object} elm Tab toggle element to mark as active 274 | */ 275 | function setActiveTab(elm, _isMatchingTabSync = false) { 276 | const activeButton = getClosest(elm, `.${classNames.tabButton}`); 277 | 278 | if (activeButton) { 279 | const activeButtonLabel = activeButton.getAttribute('data-tab'); 280 | const tabsContainer = document.querySelector(`.${classNames.tabsContainer}`); 281 | const tabBlock = activeButton.parentNode; 282 | const tabButtons = Array.apply(null, tabBlock.children).filter(elm => 283 | matchSelector(elm, 'button') 284 | ); 285 | const tabBlockOffset = tabBlock.offsetTop; 286 | 287 | tabButtons.forEach(buttonElm => buttonElm.classList.remove(classNames.tabButtonActive)); 288 | activeButton.classList.add(classNames.tabButtonActive); 289 | 290 | if (!_isMatchingTabSync) { 291 | if (settings.persist) { 292 | const tabBlocks = tabsContainer 293 | ? Array.apply(null, tabsContainer.querySelectorAll(`.${classNames.tabBlock}`)) 294 | : []; 295 | const tabBlockIndex = tabBlocks.indexOf(tabBlock); 296 | const tabStorage = JSON.parse(sessionStorage.getItem(storageKeys.persist)) || {}; 297 | 298 | tabStorage[tabBlockIndex] = activeButtonLabel; 299 | sessionStorage.setItem(storageKeys.persist, JSON.stringify(tabStorage)); 300 | } 301 | 302 | if (settings.sync) { 303 | const tabButtonMatches = tabsContainer 304 | ? Array.apply( 305 | null, 306 | tabsContainer.querySelectorAll( 307 | `.${classNames.tabButton}[data-tab="${activeButtonLabel}"]` 308 | ) 309 | ) 310 | : []; 311 | const tabStorage = JSON.parse(sessionStorage.getItem(storageKeys.sync)) || []; 312 | 313 | tabButtonMatches.forEach(tabButtonMatch => { 314 | setActiveTab(tabButtonMatch, true); 315 | }); 316 | 317 | // Maintain position in viewport when tab group's offset changes 318 | window.scrollBy(0, 0 - (tabBlockOffset - tabBlock.offsetTop)); 319 | 320 | // Remove existing label if not first in array 321 | if (tabStorage.indexOf(activeButtonLabel) > 0) { 322 | tabStorage.splice(tabStorage.indexOf(activeButtonLabel), 1); 323 | } 324 | 325 | // Add label if not already in first position 326 | if (tabStorage.indexOf(activeButtonLabel) !== 0) { 327 | tabStorage.unshift(activeButtonLabel); 328 | sessionStorage.setItem(storageKeys.sync, JSON.stringify(tabStorage)); 329 | } 330 | } 331 | } 332 | } 333 | } 334 | 335 | /** 336 | * Sets the active tab based on the anchor ID in the URL 337 | */ 338 | function setActiveTabFromAnchor() { 339 | const uriComponent = window.location.hash || window.location.search; 340 | const anchorID = decodeURIComponent((uriComponent.match(/(?:id=)([^&]+)/) || [])[1]); 341 | const anchorSelector = anchorID && `.${classNames.tabBlock} #${anchorID}`; 342 | const isAnchorElmInTabBlock = anchorID && document.querySelector(anchorSelector); 343 | 344 | if (isAnchorElmInTabBlock) { 345 | const anchorElm = document.querySelector(`#${anchorID}`); 346 | 347 | let tabContent; 348 | 349 | if (anchorElm.closest) { 350 | tabContent = anchorElm.closest(`.${classNames.tabContent}`); 351 | } else { 352 | tabContent = anchorElm.parentNode; 353 | 354 | while ( 355 | tabContent !== document.body && 356 | !tabContent.classList.contains(`${classNames.tabContent}`) 357 | ) { 358 | tabContent = tabContent.parentNode; 359 | } 360 | } 361 | 362 | setActiveTab(tabContent.previousElementSibling); 363 | } 364 | } 365 | 366 | // Plugin 367 | // ============================================================================= 368 | function docsifyTabs(hook, vm) { 369 | let hasTabs = false; 370 | 371 | hook.beforeEach(function (content) { 372 | hasTabs = regex.tabBlockMarkup.test(content); 373 | 374 | if (hasTabs) { 375 | content = renderTabsStage1(content, vm); 376 | } 377 | 378 | return content; 379 | }); 380 | 381 | hook.afterEach(function (html, next) { 382 | if (hasTabs) { 383 | html = renderTabsStage2(html); 384 | } 385 | 386 | next(html); 387 | }); 388 | 389 | hook.doneEach(function () { 390 | if (hasTabs) { 391 | setDefaultTabs(); 392 | } 393 | }); 394 | 395 | hook.mounted(function () { 396 | const tabsContainer = document.querySelector(`.${classNames.tabsContainer}`); 397 | 398 | tabsContainer && 399 | tabsContainer.addEventListener('click', function handleTabClick(evt) { 400 | setActiveTab(evt.target); 401 | }); 402 | }); 403 | } 404 | 405 | if (window) { 406 | window.$docsify = window.$docsify || {}; 407 | 408 | // Add config object 409 | window.$docsify.tabs = window.$docsify.tabs || {}; 410 | 411 | // Update settings based on $docsify config 412 | Object.keys(window.$docsify.tabs).forEach(key => { 413 | if (Object.prototype.hasOwnProperty.call(settings, key)) { 414 | settings[key] = window.$docsify.tabs[key]; 415 | } 416 | }); 417 | 418 | // Add plugin data 419 | window.$docsify.tabs.version = pkgVersion; 420 | 421 | // Init plugin 422 | if (settings.tabComments || settings.tabHeadings) { 423 | window.$docsify.plugins = [].concat(window.$docsify.plugins || [], docsifyTabs); 424 | } 425 | } 426 | -------------------------------------------------------------------------------- /src/scss/style.scss: -------------------------------------------------------------------------------- 1 | @use './../css/vars.css'; 2 | 3 | // Base 4 | // ============================================================================= 5 | .docsify-tabs:before, 6 | .docsify-tabs__tab { 7 | z-index: 1; 8 | } 9 | 10 | .docsify-tabs__tab:focus, 11 | .docsify-tabs__tab--active { 12 | z-index: 2; 13 | } 14 | 15 | .docsify-tabs { 16 | display: flex; 17 | flex-wrap: wrap; 18 | position: relative; 19 | 20 | &:before { 21 | content: ''; 22 | order: 0; 23 | flex: 1; 24 | } 25 | } 26 | 27 | .docsify-tabs__tab { 28 | order: -1; 29 | position: relative; 30 | margin: 0; 31 | font-size: inherit; 32 | appearance: none; 33 | } 34 | 35 | .docsify-tabs__content[class] { 36 | // Add weight instead of !important 37 | visibility: hidden; 38 | position: absolute; 39 | overflow: hidden; 40 | height: 0; 41 | width: 100%; 42 | 43 | > :first-child { 44 | margin-top: 0; 45 | } 46 | 47 | > :last-child { 48 | margin-bottom: 0; 49 | } 50 | 51 | .docsify-tabs__tab--active + & { 52 | visibility: visible; 53 | position: relative; 54 | overflow: auto; 55 | height: auto; 56 | } 57 | } 58 | 59 | // Themes 60 | // ============================================================================= 61 | [class*='docsify-tabs--'] { 62 | margin: var(--docsifytabs-margin); 63 | 64 | > .docsify-tabs__tab { 65 | padding: var(--docsifytabs-tab-padding); 66 | background: var(--docsifytabs-tab-background); 67 | color: var(--docsifytabs-tab-color); 68 | } 69 | 70 | > .docsify-tabs__tab--active { 71 | background: var(--docsifytabs-tab-background--active); 72 | color: var(--docsifytabs-tab-color--active); 73 | } 74 | 75 | > .docsify-tabs__content { 76 | background: var(--docsifytabs-content-background); 77 | } 78 | 79 | > .docsify-tabs__tab--active + .docsify-tabs__content { 80 | padding: var(--docsifytabs-content-padding); 81 | } 82 | } 83 | 84 | // Classic 85 | // ----------------------------------------------------------------------------- 86 | .docsify-tabs--classic { 87 | &:before, 88 | > .docsify-tabs__tab, 89 | > .docsify-tabs__content { 90 | border-width: var(--docsifytabs-border-px); 91 | border-style: solid; 92 | border-color: var(--docsifytabs-border-color); 93 | } 94 | 95 | &:before { 96 | margin-right: var(--docsifytabs-border-px); 97 | border-top-width: 0; 98 | border-left-width: 0; 99 | border-right-width: 0; 100 | } 101 | 102 | > .docsify-tabs__tab { 103 | &:first-of-type { 104 | border-top-left-radius: var(--docsifytabs-border-radius-px); 105 | } 106 | 107 | &:last-of-type { 108 | border-top-right-radius: var(--docsifytabs-border-radius-px); 109 | } 110 | } 111 | 112 | > .docsify-tabs__tab ~ .docsify-tabs__tab { 113 | margin-left: calc(0px - var(--docsifytabs-border-px)); 114 | } 115 | 116 | > .docsify-tabs__tab--active { 117 | border-bottom-width: 0; 118 | box-shadow: inset 0 var(--docsifytabs-tab-highlight-px) 0 0 119 | var(--docsifytabs-tab-highlight-color); 120 | } 121 | 122 | > .docsify-tabs__content { 123 | margin-top: calc(0px - var(--docsifytabs-border-px)); 124 | border-top: 0; 125 | border-radius: 0 var(--docsifytabs-border-radius-px) var(--docsifytabs-border-radius-px) 126 | var(--docsifytabs-border-radius-px); 127 | } 128 | } 129 | 130 | // Material 131 | // ----------------------------------------------------------------------------- 132 | .docsify-tabs--material { 133 | > .docsify-tabs__tab { 134 | margin-bottom: calc(var(--docsifytabs-tab-highlight-px) - var(--docsifytabs-border-px)); 135 | background: transparent; 136 | border: 0; 137 | } 138 | 139 | > .docsify-tabs__tab--active { 140 | box-shadow: 0 var(--docsifytabs-tab-highlight-px) 0 0 var(--docsifytabs-tab-highlight-color); 141 | background: transparent; 142 | } 143 | 144 | > .docsify-tabs__content { 145 | border-width: var(--docsifytabs-border-px) 0; 146 | border-style: solid; 147 | border-color: var(--docsifytabs-border-color); 148 | } 149 | } 150 | --------------------------------------------------------------------------------