├── .npmignore ├── .README ├── tweet-button.png ├── download.md ├── configuration.md ├── README.md ├── content-indexing.md ├── markup.md ├── events.md ├── usage.md ├── similar-libraries.md └── linking.md ├── examples ├── good-looking │ ├── punch.jpg │ ├── mint-julep.jpg │ ├── main.css │ ├── main.scss │ └── index.html ├── plain │ ├── main.scss │ ├── main.css │ ├── example.css │ ├── example.scss │ └── index.html ├── jquery │ └── index.html ├── list-element │ └── index.html ├── smooth-scrolling │ └── index.html └── events │ └── index.html ├── .eslintrc ├── test ├── .eslintrc ├── fixtures │ └── page.html └── contents.js ├── .babelrc ├── .gitignore ├── .travis.yml ├── package.json ├── LICENSE ├── README.md └── src └── index.js /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | test 3 | coverage 4 | Dockerfile 5 | .* 6 | *.log 7 | -------------------------------------------------------------------------------- /.README/tweet-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gajus/contents/HEAD/.README/tweet-button.png -------------------------------------------------------------------------------- /examples/good-looking/punch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gajus/contents/HEAD/examples/good-looking/punch.jpg -------------------------------------------------------------------------------- /examples/good-looking/mint-julep.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gajus/contents/HEAD/examples/good-looking/mint-julep.jpg -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "canonical" 4 | ], 5 | "rules": { 6 | "filenames/match-exported": 0 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.README/download.md: -------------------------------------------------------------------------------- 1 | ## Download 2 | 3 | Using [NPM](https://www.npmjs.org/): 4 | 5 | ```sh 6 | npm install contents 7 | 8 | ``` 9 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "canonical/mocha" 4 | ], 5 | "rules": { 6 | "mocha/no-setup-in-describe": 0 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "targets": ">1%, not dead" 7 | } 8 | ] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | *.log 5 | .* 6 | !.babelrc 7 | !.editorconfig 8 | !.eslintrc 9 | !.flowconfig 10 | !.gitignore 11 | !.gitlab-ci.yml 12 | !.npmignore 13 | !.README 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | - 8 5 | script: 6 | - export NODE_ENV=test 7 | - npm run build 8 | - npm run test 9 | - npm run lint 10 | notifications: 11 | email: false 12 | sudo: false 13 | -------------------------------------------------------------------------------- /.README/configuration.md: -------------------------------------------------------------------------------- 1 | ## Configuration 2 | 3 | | Name | Type | Description | 4 | | --- | --- | --- | 5 | | `articles` | `NodeList`, `jQuery` | (optional) The default behavior is to index all headings (H1-H6) in the document. See [Content Indexing](#content-indexing). | 6 | | `link` | `function` | (optional) Used to represent article in the table of contents and to setup navigation. See [Linking](#linking). | -------------------------------------------------------------------------------- /.README/README.md: -------------------------------------------------------------------------------- 1 | # Table of Contents (TOC) Generator 2 | 3 | {"gitdown": "badge", "name": "travis"} 4 | {"gitdown": "badge", "name": "npm-version"} 5 | 6 | 9 | 10 | Table of contents generator. 11 | 12 | {"gitdown": "contents"} 13 | 14 | {"gitdown": "include", "file": "./usage.md"} 15 | {"gitdown": "include", "file": "./similar-libraries.md"} 16 | {"gitdown": "include", "file": "./download.md"} 17 | {"gitdown": "include", "file": "./configuration.md"} 18 | {"gitdown": "include", "file": "./content-indexing.md"} 19 | {"gitdown": "include", "file": "./linking.md"} 20 | {"gitdown": "include", "file": "./markup.md"} 21 | {"gitdown": "include", "file": "./events.md"} 22 | -------------------------------------------------------------------------------- /test/fixtures/page.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

OK

5 |
6 |
7 |

A1

8 |

B1

9 |

C1

10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | 27 | 44 | -------------------------------------------------------------------------------- /.README/content-indexing.md: -------------------------------------------------------------------------------- 1 | ## Content Indexing 2 | 3 | The default behavior is to index all headings (H1-H6) in the document. 4 | 5 | Use `articles` setting to index content using your own selector: 6 | 7 | ```js 8 | Contents({ 9 | articles: document.querySelectorAll('main h2, main h2') 10 | // If you are using jQuery 11 | // articles: $('main').find('h2, h3').get() 12 | }); 13 | 14 | ``` 15 | 16 | ### Hierarchy 17 | 18 | `articles` will be used to make the table of contents. `articles` have level of importance. The level of importance determines list nesting (see [Markup](#markup)). For HTML headings, the level of importance is derived from the tag name (``). To set your own level of importance, use `Contents.level` [dataset](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement.dataset) property or jQuery data property with the same name, e.g. 19 | 20 | ```js 21 | $('main').find('.summary').data('gajus.contents.level', 4); 22 | 23 | Contents({ 24 | articles: $('main').find('h1, h2, h3, .summary').get() 25 | }); 26 | 27 | ``` 28 | 29 | When level of importance cannot be determined, it defaults to 1. 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "email": "gajus@gajus.com", 4 | "name": "Gajus Kuizinas", 5 | "url": "http://gajus.com" 6 | }, 7 | "dependencies": { 8 | "lodash": "^4.17.10", 9 | "sister": "^3.0.1" 10 | }, 11 | "description": "Table of contents generator.", 12 | "devDependencies": { 13 | "@babel/cli": "^7.0.0", 14 | "@babel/core": "^7.0.0", 15 | "@babel/preset-env": "^7.0.0", 16 | "@babel/register": "^7.0.0", 17 | "chai": "^4.1.2", 18 | "eslint-config-canonical": "^12.0.0", 19 | "gitdown": "^2.5.3", 20 | "jsdom": "^12.0.0", 21 | "mocha": "^5.2.0" 22 | }, 23 | "keywords": [ 24 | "table of contents", 25 | "toc", 26 | "contents" 27 | ], 28 | "license": "BSD-3-Clause", 29 | "main": "./dist/index.js", 30 | "name": "contents", 31 | "repository": { 32 | "type": "git", 33 | "url": "http://github.com/gajus/contents.git" 34 | }, 35 | "scripts": { 36 | "build": "rm -fr ./dist && NODE_ENV=production babel ./src --out-dir ./dist --copy-files --source-maps", 37 | "generate-readme": "gitdown ./.README/README.md --output-file ./README.md", 38 | "lint": "eslint ./src", 39 | "test": "mocha --require @babel/register" 40 | }, 41 | "title": "Contents", 42 | "version": "5.0.0" 43 | } 44 | -------------------------------------------------------------------------------- /.README/markup.md: -------------------------------------------------------------------------------- 1 | ## Markup 2 | 3 | Table of contents is an ordered [list element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ol). List nesting reflects the heading hierarchy. The default behavior is to represent each heading using a hyperlink (See [Linking](#linking)), e.g. 4 | 5 | ```html 6 |

JavaScript

7 |

History

8 |

Trademark

9 |

Features

10 |

Imperative and structured

11 |

Dynamic

12 |

Functional

13 |

Syntax

14 | ``` 15 | 16 | Contents will generate the following markup for the above content: 17 | 18 | ```html 19 |
    20 |
  1. 21 | JavaScript 22 | 23 |
      24 |
    1. 25 | History 26 |
    2. 27 |
    3. 28 | Trademark 29 |
    4. 30 |
    5. 31 | Features 32 | 33 |
        34 |
      1. 35 | Imperative and structured 36 |
      2. 37 |
      3. 38 | Dynamic 39 |
      4. 40 |
      5. 41 | Functional 42 |
      6. 43 |
      44 |
    6. 45 |
    7. 46 | Syntax 47 |
    8. 48 |
    49 |
  2. 50 |
51 | ``` 52 | -------------------------------------------------------------------------------- /.README/events.md: -------------------------------------------------------------------------------- 1 | ## Events 2 | 3 | | Event | Description | 4 | | --- | --- | 5 | | `ready` | Fired once after the table of contents has been generated. | 6 | | `resize` | Fired when the page is loaded and in response to "resize" and "orientationchange" `window` events. | 7 | | `change` | Fired when the page is loaded and when user navigates to a new section of the page. | 8 | 9 | Attach event listeners using the `eventEmitter.on` of the resulting Contents object: 10 | 11 | ```js 12 | const contents = Contents(); 13 | 14 | contents.eventEmitter.on('ready', () => {}); 15 | contents.eventEmitter.on('resize', () => {}); 16 | 17 | ``` 18 | 19 | The `change` event listener is passed extra parameters: `.current.article`, `.current.guide`, and when available, `.previous.article`, `.previous.guide`: 20 | 21 | ```js 22 | contents.eventEmitter.on('change', (data) => { 23 | if (data.previous) { 24 | $(data.previous.article).removeClass('active-article'); 25 | $(data.previous.guide).removeClass('active-guide'); 26 | } 27 | 28 | $(data.current.article).addClass('active-article'); 29 | $(data.current.guide).addClass('active-guide'); 30 | }); 31 | 32 | ``` 33 | 34 | You must trigger "resize" event after programmatically changing the content or the presentation of the content.: 35 | 36 | ```js 37 | contents.eventEmitter.trigger('resize'); 38 | 39 | ``` 40 | 41 | This is required to recalculate the position of the content. 42 | -------------------------------------------------------------------------------- /examples/plain/main.scss: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; padding: 0; 3 | } 4 | 5 | body { 6 | font: normal 16px/24px 'Helvetica Neue', Helvetica, Arial, freesans, sans-serif; 7 | } 8 | 9 | main { 10 | width: 960px; margin: 0 auto; overflow: hidden; 11 | } 12 | 13 | #contents { 14 | position: fixed; top: 40px; width: 320px; 15 | 16 | ol { 17 | counter-reset: section; 18 | list-style-type: none; 19 | } 20 | 21 | li:before { 22 | counter-increment: section; 23 | content: counters(section, '.') '. '; 24 | } 25 | 26 | > ol { 27 | background: #FFF8DC; border: 1px solid #E0DCBF; padding: 10px; 28 | } 29 | 30 | a { 31 | color: #0000ee; text-decoration: none; 32 | 33 | &:hover { 34 | text-decoration: underline; 35 | } 36 | } 37 | } 38 | 39 | article { 40 | overflow: hidden; position: relative; margin: 40px 0 40px 340px; background: #fff; border: 1px solid #ccc; padding: 20px 40px 40px 40px; 41 | 42 | h1, 43 | h2, 44 | h3, 45 | h4 { 46 | font-size: 30px; line-height: 40px; margin: 0 0 10px 0; padding: 0; font-weight: bold; 47 | } 48 | 49 | h1 { 50 | margin: 0 0 20px 0; padding-top: 20px; 51 | } 52 | 53 | h2, 54 | h3, 55 | h4 { 56 | font-size: 20px; line-height: 30px; padding-top: 20px; 57 | } 58 | 59 | h3, 60 | h4 { font-size: 16px; line-height: 24px; } 61 | 62 | p { 63 | margin: 10px 0; 64 | } 65 | } -------------------------------------------------------------------------------- /examples/plain/main.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; } 4 | 5 | body { 6 | font: normal 16px/24px 'Helvetica Neue', Helvetica, Arial, freesans, sans-serif; } 7 | 8 | main { 9 | width: 960px; 10 | margin: 0 auto; 11 | overflow: hidden; } 12 | 13 | #contents { 14 | position: fixed; 15 | top: 40px; 16 | width: 320px; } 17 | #contents ol { 18 | counter-reset: section; 19 | list-style-type: none; } 20 | #contents li:before { 21 | counter-increment: section; 22 | content: counters(section, '.') '. '; } 23 | #contents > ol { 24 | background: #FFF8DC; 25 | border: 1px solid #E0DCBF; 26 | padding: 10px; } 27 | #contents a { 28 | color: #0000ee; 29 | text-decoration: none; } 30 | #contents a:hover { 31 | text-decoration: underline; } 32 | 33 | article { 34 | overflow: hidden; 35 | position: relative; 36 | margin: 40px 0 40px 340px; 37 | background: #fff; 38 | border: 1px solid #ccc; 39 | padding: 20px 40px 40px 40px; } 40 | article h1, article h2, article h3, article h4 { 41 | font-size: 30px; 42 | line-height: 40px; 43 | margin: 0 0 10px 0; 44 | padding: 0; 45 | font-weight: bold; } 46 | article h1 { 47 | margin: 0 0 20px 0; 48 | padding-top: 20px; } 49 | article h2, article h3, article h4 { 50 | font-size: 20px; 51 | line-height: 30px; 52 | padding-top: 20px; } 53 | article h3, article h4 { 54 | font-size: 16px; 55 | line-height: 24px; } 56 | article p { 57 | margin: 10px 0; } 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018, Gajus Kuizinas (http://gajus.com/) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the Gajus Kuizinas (http://gajus.com/) nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL ANUARY BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /examples/plain/example.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; } 4 | 5 | body { 6 | font: normal 16px/24px 'Helvetica Neue', Helvetica, Arial, freesans, sans-serif; } 7 | 8 | main { 9 | width: 960px; 10 | margin: 0 auto; 11 | overflow: hidden; } 12 | 13 | #contents { 14 | position: fixed; 15 | top: 40px; 16 | width: 320px; } 17 | #contents ol { 18 | counter-reset: section; 19 | list-style-type: none; } 20 | #contents li:before { 21 | counter-increment: section; 22 | content: counters(section, '.') '. '; } 23 | #contents > ol { 24 | background: #FFF8DC; 25 | border: 1px solid #E0DCBF; 26 | padding: 10px; } 27 | #contents a { 28 | color: #0000ee; 29 | text-decoration: none; } 30 | #contents a:hover { 31 | text-decoration: underline; } 32 | #contents .active-anchor > a { 33 | color: #f00; } 34 | #contents .active-child > a::after { 35 | content: ' (active child)'; 36 | text-decoration: none; 37 | color: #444; } 38 | 39 | article { 40 | overflow: hidden; 41 | position: relative; 42 | margin: 40px 0 40px 340px; 43 | background: #fff; 44 | border: 1px solid #ccc; 45 | padding: 20px 40px 40px 40px; } 46 | article h1, article h2, article h3, article h4 { 47 | font-size: 30px; 48 | line-height: 40px; 49 | margin: 0 0 10px 0; 50 | padding: 0; 51 | font-weight: bold; } 52 | article h1 { 53 | margin: 0 0 20px 0; 54 | padding-top: 20px; } 55 | article h2, article h3, article h4 { 56 | font-size: 20px; 57 | line-height: 30px; 58 | padding-top: 20px; } 59 | article h3, article h4 { 60 | font-size: 16px; 61 | line-height: 24px; } 62 | article p { 63 | margin: 10px 0; } 64 | -------------------------------------------------------------------------------- /examples/plain/example.scss: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; padding: 0; 3 | } 4 | 5 | body { 6 | font: normal 16px/24px 'Helvetica Neue', Helvetica, Arial, freesans, sans-serif; 7 | } 8 | 9 | main { 10 | width: 960px; margin: 0 auto; overflow: hidden; 11 | } 12 | 13 | #contents { 14 | position: fixed; top: 40px; width: 320px; 15 | 16 | ol { 17 | counter-reset: section; 18 | list-style-type: none; 19 | } 20 | 21 | li:before { 22 | counter-increment: section; 23 | content: counters(section, '.') '. '; 24 | } 25 | 26 | > ol { 27 | background: #FFF8DC; border: 1px solid #E0DCBF; padding: 10px; 28 | } 29 | 30 | a { 31 | color: #0000ee; text-decoration: none; 32 | 33 | &:hover { 34 | text-decoration: underline; 35 | } 36 | } 37 | 38 | .active-anchor { 39 | > a { 40 | color: #f00; 41 | } 42 | } 43 | 44 | .active-child { 45 | > a { 46 | &::after { content: ' (active child)'; text-decoration: none; color: #444; } 47 | } 48 | } 49 | } 50 | 51 | article { 52 | overflow: hidden; position: relative; margin: 40px 0 40px 340px; background: #fff; border: 1px solid #ccc; padding: 20px 40px 40px 40px; 53 | 54 | h1, 55 | h2, 56 | h3, 57 | h4 { 58 | font-size: 30px; line-height: 40px; margin: 0 0 10px 0; padding: 0; font-weight: bold; 59 | } 60 | 61 | h1 { 62 | margin: 0 0 20px 0; padding-top: 20px; 63 | } 64 | 65 | h2, 66 | h3, 67 | h4 { 68 | font-size: 20px; line-height: 30px; padding-top: 20px; 69 | } 70 | 71 | h3, 72 | h4 { font-size: 16px; line-height: 24px; } 73 | 74 | p { 75 | margin: 10px 0; 76 | } 77 | } -------------------------------------------------------------------------------- /.README/usage.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | ### Quick Start 4 | 5 | ```js 6 | import Contents from 'contents'; 7 | 8 | // This example generates a table of contents for all of the headings in the document. 9 | // Table of contents is an ordered list element. 10 | const contents = Contents(); 11 | 12 | // Append the generated list element (table of contents) to the container. 13 | document.querySelector('#your-table-of-contents-container').appendChild(contents.list()); 14 | 15 | // Attach event listeners: 16 | contents.eventEmitter().on('change', function () { 17 | console.log('User has navigated to a new section of the page.'); 18 | }); 19 | 20 | // The rest of the code illustrates firing "resize" event after you have 21 | // added new content after generating the table of contents. 22 | const newHeading = document.createElement('h2'); 23 | 24 | hewHeading.innerHTML = 'Dynamically generated title'; 25 | 26 | document.body.appendChild(newHeading); 27 | 28 | // Firing the "resize" event will regenerate the table of contents. 29 | contents.eventEmitter().trigger('resize'); 30 | 31 | ``` 32 | 33 | ### Examples 34 | 35 | * [Good looking](http://gajus.com/sandbox/contents/examples/good-looking/) example. 36 | * [Plain](http://gajus.com/sandbox/contents/examples/plain/) table of contents not using jQuery. 37 | * [Events](http://gajus.com/sandbox/contents/examples/events/) table of contents with all events logged in the `console.log`. 38 | * [Obtain Generated List Element](http://gajus.com/sandbox/contents/examples/list-element/). 39 | * [jQuery](http://gajus.com/sandbox/contents/examples/jquery/) table of contents using jQuery. 40 | * [Smooth scrolling](http://gajus.com/sandbox/contents/examples/smooth-scrolling/) implemented using [jquery-smooth-scroll](https://github.com/kswedberg/jquery-smooth-scroll). 41 | 42 | The code for all of the examples is in the [examples](./examples/) folder. 43 | 44 | [Raise an issue](https://github.com/gajus/contents/issues) if you are missing an example. 45 | -------------------------------------------------------------------------------- /.README/similar-libraries.md: -------------------------------------------------------------------------------- 1 | ## Introduction of ES6 in 4.0.0 2 | 3 | [Similar Libraries](#rimilar-libraries) stats have been generated in 22-Nov-14 08:44:41 UTC. Since then Contents has evolved a lot. The source code is written in ES6 and depends on `babel-core` to run. In projects that already depend on Babel and use webpack to build packages, this is not going to be a problem. Other projects need to consider the relatively heavy weight of the generated package. 4 | 5 | ## Similar Libraries 6 | 7 | | Feature | [contents](https://github.com/gajus/contents) | [toc](https://github.com/jgallen23/toc) | [jquery.tocify.js](https://github.com/gfranko/jquery.tocify.js) | 8 | | --- | --- | --- | --- | 9 | | Markup using nested `
    ` | ✓ | - | - | 10 | | [Smooth scrolling](#smooth-scrolling) | - | ✓ | ✓ | 11 | | Forward and back button support | ✓ | - | ✓ | 12 | | [Events](#events) | ✓ | - | - | 13 | | [Efficient `scroll` event](#window-resize-and-scroll-event-handling) | ✓ | ✓ | - | 14 | | [Reflect `window` resize](#window-resize-and-scroll-event-handling) | ✓ | - | ✓ | 15 | | [Extract table of contents as an array](#table-of-contents-array) | ✓ | - | - | 16 | | Overwrite markup and navigation | ✓ | - | - | 17 | | Can have multiple on a page | ✓ | ✓ | ✓ | 18 | | [Required 3rd party libraries](#required-3rd-party-libraries) | - | jQuery | jQuery, jQueryUI | 19 | | Size | < 6.000 kb | 2.581 kb | 7.246 kb | 20 | | GitHub Stars | 192 | 307 | 435 | 21 | 22 | Last updated: Saturday, 22-Nov-14 08:44:41 UTC. 23 | 24 | ### Required 3rd Party Libraries 25 | 26 | There are no 3rd party dependencies. jQuery selectors are used in the examples to make it simple for the reader. 27 | 28 | ### Smooth Scrolling 29 | 30 | You can implement smooth scrolling using either of the existing libraries. See [Integration Examples](#integration-examples). 31 | 32 | ### Window Resize and `scroll` Event Handling 33 | 34 | The library will index `offsetTop` of all articles. This index is used to reflect the [change event](#events). The index is built upon loading the page, and in response to `window.onresize` and [`ready`](#events) events. 35 | 36 | Reading `offsetTop` causes a [reflow](http://gent.ilcore.com/2011/03/how-not-to-trigger-layout-in-webkit.html). Therefore, this should not be done while scrolling. 37 | 38 | # Table of Contents Array 39 | 40 | You can extract the table of contents as a collection of nested objects representing the table of contents. 41 | 42 | ```js 43 | /** 44 | * @return {array} Array representation of the table of contents. 45 | */ 46 | contents.tree(); 47 | ``` 48 | 49 | Tree is a collection of nodes: 50 | 51 | ```js 52 | [ 53 | // Node 54 | { 55 | // Hierarchy level (e.g. h1 = 1) 56 | level: 1, 57 | // Id derived using articleId() function. 58 | id: '', 59 | // Name derived using articleName() function. 60 | name: '', 61 | // The article element. 62 | element: null, 63 | // Collection of the descendant nodes. 64 | descendants: [ /* node */ ] 65 | } 66 | ] 67 | ``` 68 | -------------------------------------------------------------------------------- /.README/linking.md: -------------------------------------------------------------------------------- 1 | ## Linking 2 | 3 | `link` method is used to represent article in the table of contents and to setup navigation. This method is called once for each article after the list of the table of contents is generated. 4 | 5 | The default implementation: 6 | 7 | 1. Derives ID from the article 8 | 2. Generates a hyperlink using article ID as the anchor 9 | 3. Appends the URL to the table of contents 10 | 4. Wraps the article node in a self-referencing hyperlink. 11 | 12 | ```js 13 | /** 14 | * This function is called after the table of contents is generated. 15 | * It is called for each article in the index. 16 | * Used to represent article in the table of contents and to setup navigation. 17 | * 18 | * @param {HTMLElement} guide An element in the table of contents representing an article. 19 | * @param {HTMLElement} article The represented content element. 20 | */ 21 | Contents.link = (guide, article) => { 22 | const guideLink = document.createElement('a'), 23 | const articleLink = document.createElement('a'), 24 | const articleName = article.innerText, 25 | const articleId = article.id || Contents.id(articleName); 26 | 27 | article.id = articleId; 28 | 29 | articleLink.href = '#' + articleId; 30 | 31 | while (article.childNodes.length) { 32 | articleLink.appendChild(article.childNodes[0], articleLink); 33 | } 34 | 35 | article.appendChild(articleLink); 36 | 37 | guideLink.appendChild(document.createTextNode(articleName)); 38 | guideLink.href = '#' + articleId; 39 | guide.insertBefore(guideLink, guide.firstChild); 40 | }; 41 | 42 | ``` 43 | 44 | To overwrite the default behavior, you can provide your own `link` function as part of the configuration: 45 | 46 | ```js 47 | Contents({ 48 | // Example of implementation that does not wrap 49 | // article node in a hyperlink. 50 | link: (guide, article) => { 51 | var guideLink, 52 | articleName, 53 | articleId; 54 | 55 | guide = $(guide); 56 | article = $(article); 57 | 58 | guideLink = $(''); 59 | articleName = article.text(); 60 | articleId = article.attr('id') || Contents.id(articleName); 61 | 62 | guideLink 63 | .text(articleName) 64 | .attr('href', '#' + articleId) 65 | .prependTo(guide); 66 | 67 | article.attr('id', articleId); 68 | } 69 | }); 70 | 71 | ``` 72 | 73 | ### Article ID 74 | 75 | The default implementation relies on each article having an "id" attribute to enable anchor navigation. 76 | 77 | If you are overwriting the default `link` implementation, you can take advantage of the `Contents.id` function. 78 | 79 | `Contents.id` is responsible for deriving a unique ID from the text of the article, e.g. 80 | 81 | ```html 82 |

    Allow me to reiterate

    83 |

    Allow me to reiterate

    84 |

    Allow me to reiterate

    85 | 86 | ``` 87 | 88 | The default `link` implementation will use `Contents.id` to give each article a unique ID: 89 | 90 | ```html 91 |

    Allow me to reiterate

    92 |

    Allow me to reiterate

    93 |

    Allow me to reiterate

    94 | 95 | ``` 96 | -------------------------------------------------------------------------------- /examples/good-looking/main.css: -------------------------------------------------------------------------------- 1 | body, main, aside, article, ul, ol, li, h1, h2, h3, hr, p { 2 | display: block; 3 | margin: 0; 4 | padding: 0; } 5 | 6 | body { 7 | font: normal 16px/24px 'Helvetica Neue', Helvetica, Arial, freesans, sans-serif; 8 | background: #F8F9FA; } 9 | 10 | aside { 11 | position: fixed; 12 | left: 0; 13 | top: 0; 14 | width: 360px; } 15 | aside li { 16 | display: block; 17 | z-index: 10; } 18 | aside li a { 19 | display: block; 20 | padding: 10px; 21 | text-decoration: none; 22 | color: #666; 23 | border: 1px solid #D9E0E2; 24 | background: #fff; 25 | width: 280px; 26 | position: relative; } 27 | aside > ol > li ol { 28 | margin: 10px 0 10px 20px; } 29 | aside > ol > li ol a { 30 | border-bottom: none; } 31 | aside > ol > li ol li:last-child a { 32 | border-bottom: 1px solid #D9E0E2; } 33 | aside > ol { 34 | padding: 20px; } 35 | aside > ol > li.active > a, aside > ol > li.active-child > a { 36 | background: #EF4836; 37 | color: #fff; } 38 | aside > ol > li.active > a::after { 39 | content: '•'; 40 | font-size: 45px; 41 | line-height: 45px; 42 | position: absolute; 43 | right: 10px; 44 | top: 0; 45 | color: #fff; } 46 | aside > ol > li li.active > a::after, aside > ol > li li.active-child > a::after { 47 | content: '•'; 48 | font-size: 45px; 49 | line-height: 45px; 50 | position: absolute; 51 | right: 10px; 52 | top: 0; 53 | color: #EF4836; } 54 | aside > ol:hover > li.active > a, aside > ol:hover > li.active-child > a { 55 | background: #666; } 56 | aside > ol:hover > li li.active > a::after, aside > ol:hover > li li.active-child > a::after { 57 | color: #666; } 58 | aside > ol > li:hover > a { 59 | background: #EF4836 !important; 60 | color: #fff; } 61 | aside > ol > li li:hover > a::after { 62 | content: '•'; 63 | font-size: 45px; 64 | line-height: 45px; 65 | position: absolute; 66 | right: 10px; 67 | top: 0; 68 | color: #EF4836 !important; } 69 | aside > ol li.active ol, aside > ol li.active-child ol, aside > ol li:hover ol { 70 | opacity: 1; } 71 | aside > ol ol { 72 | opacity: 0.5; } 73 | aside > ol > li { 74 | counter-reset: drink; } 75 | aside > ol > li li a::before { 76 | counter-increment: drink; 77 | content: counter(drink) '. '; } 78 | 79 | main { 80 | margin: 0 0 0 340px; 81 | padding: 0 20px; } 82 | main hr { 83 | border: none; 84 | height: 1px; } 85 | 86 | article { 87 | width: 680px; 88 | min-height: 930px; 89 | box-sizing: border-box; 90 | margin: 50px auto; 91 | background: #fff; 92 | border: 1px solid #D9E0E2; 93 | overflow: hidden; 94 | padding: 60px 100px; 95 | font: 16px/22px Merriweather, 'times new roman', serif; } 96 | article h1, article h2 { 97 | text-align: center; } 98 | article h1 a, article h2 a { 99 | color: #FF3C1F; 100 | text-decoration: none; } 101 | article h1 { 102 | margin: 0 0 20px 0; 103 | font-size: 30px; 104 | line-height: 40px; 105 | font-weight: 700; 106 | counter-reset: drink; 107 | text-transform: uppercase; } 108 | article h2 { 109 | padding: 20px 0 0 0; 110 | font-size: 20px; 111 | font-weight: 400; 112 | line-height: 30px; } 113 | article h2::before { 114 | counter-increment: drink; 115 | content: counter(drink) '.'; 116 | font-size: 25px; 117 | font-weight: 300; } 118 | article p { 119 | margin: 20px 0; } 120 | article img { 121 | display: block; 122 | margin: 40px auto; 123 | max-width: 100%; } 124 | article .serving { 125 | text-align: center; 126 | font-weight: 300; } 127 | article .serving::before { 128 | content: '('; } 129 | article .serving::after { 130 | content: ')'; } 131 | article .ingredients { 132 | margin: 20px; } 133 | article .ingredients li { 134 | list-style: none; } 135 | article .making { 136 | text-indent: 20px; } 137 | 138 | #source { 139 | color: #666; 140 | margin: 20px; 141 | text-align: center; } 142 | #source a { 143 | color: inherit; } 144 | -------------------------------------------------------------------------------- /examples/good-looking/main.scss: -------------------------------------------------------------------------------- 1 | body, 2 | main, 3 | aside, 4 | article, 5 | ul, 6 | ol, 7 | li, 8 | h1, 9 | h2, 10 | h3, 11 | hr, 12 | p { 13 | display: block; margin: 0; padding: 0; 14 | } 15 | 16 | body { 17 | font: normal 16px/24px 'Helvetica Neue', Helvetica, Arial, freesans, sans-serif; background: #F8F9FA; 18 | } 19 | 20 | aside { 21 | position: fixed; left: 0; top: 0; width: 360px; 22 | 23 | li { 24 | display: block; z-index: 10; 25 | 26 | a { 27 | display: block; padding: 10px; text-decoration: none; color: #666; border: 1px solid #D9E0E2; background: #fff; width: 280px; position: relative; 28 | } 29 | } 30 | 31 | // Dimensional 32 | 33 | > ol > li { 34 | ol { 35 | margin: 10px 0 10px 20px; 36 | 37 | a { 38 | border-bottom: none; 39 | } 40 | 41 | li:last-child a { 42 | border-bottom: 1px solid #D9E0E2; 43 | } 44 | } 45 | } 46 | 47 | // Color 48 | 49 | > ol { 50 | padding: 20px; 51 | 52 | > li.active > a, 53 | > li.active-child > a { 54 | background: #EF4836; color: #fff; 55 | } 56 | 57 | > li.active > a { 58 | &::after { 59 | content: '•'; font-size: 45px; line-height: 45px; position: absolute; right: 10px; top: 0; color: #fff; 60 | } 61 | } 62 | 63 | > li li.active > a, 64 | > li li.active-child > a { 65 | &::after { 66 | content: '•'; font-size: 45px; line-height: 45px; position: absolute; right: 10px; top: 0; color: #EF4836; 67 | } 68 | } 69 | 70 | &:hover { 71 | > li.active > a, 72 | > li.active-child > a { 73 | background: #666; 74 | } 75 | 76 | > li li.active > a, 77 | > li li.active-child > a { 78 | &::after { 79 | color: #666; 80 | } 81 | } 82 | } 83 | 84 | > li:hover > a { 85 | background: #EF4836!important; color: #fff; 86 | } 87 | 88 | > li li:hover > a { 89 | &::after { 90 | content: '•'; font-size: 45px; line-height: 45px; position: absolute; right: 10px; top: 0; color: #EF4836!important; 91 | } 92 | } 93 | 94 | li.active, 95 | li.active-child, 96 | li:hover { 97 | ol { 98 | opacity: 1; 99 | } 100 | } 101 | 102 | ol { 103 | opacity: .5; 104 | } 105 | } 106 | 107 | // Counter 108 | 109 | > ol > li { 110 | counter-reset: drink; 111 | 112 | li a::before { 113 | counter-increment: drink; content: counter(drink) '. '; 114 | } 115 | } 116 | } 117 | 118 | main { 119 | margin: 0 0 0 340px; padding: 0 20px; 120 | 121 | hr { 122 | border: none; height: 1px; 123 | } 124 | } 125 | 126 | article { 127 | width: 680px; min-height: 930px; box-sizing: border-box; margin: 50px auto; background: #fff; border: 1px solid #D9E0E2; overflow: hidden; padding: 60px 100px; font: 16px/22px Merriweather, 'times new roman', serif; 128 | 129 | h1, 130 | h2 { 131 | text-align: center; 132 | 133 | a { color: #FF3C1F; text-decoration: none; } 134 | } 135 | 136 | h1 { 137 | margin: 0 0 20px 0; font-size: 30px; line-height: 40px; font-weight: 700; counter-reset: drink; text-transform: uppercase; 138 | } 139 | 140 | h2 { 141 | padding: 20px 0 0 0; font-size: 20px; font-weight: 400; line-height: 30px; 142 | 143 | &::before { 144 | counter-increment: drink; content: counter(drink) '.'; font-size: 25px; font-weight: 300; 145 | } 146 | } 147 | 148 | p { 149 | margin: 20px 0; 150 | } 151 | 152 | img { 153 | display: block; margin: 40px auto; max-width: 100%; 154 | } 155 | 156 | .serving { 157 | text-align: center; font-weight: 300; 158 | 159 | &::before { 160 | content: '('; 161 | } 162 | &::after { 163 | content: ')'; 164 | } 165 | } 166 | 167 | .ingredients { 168 | margin: 20px; 169 | 170 | li { list-style: none; } 171 | } 172 | 173 | .making { 174 | text-indent: 20px; 175 | } 176 | } 177 | 178 | #source { 179 | color: #666; margin: 20px; text-align: center; 180 | 181 | a { color: inherit; } 182 | } -------------------------------------------------------------------------------- /examples/jquery/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 |
    18 | 19 |
    20 |

    Pellentesque habitant

    21 | 22 |

    Morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus

    23 | 24 |

    Etiam dignissim interdum

    25 | 26 |

    Mauris sed diam vel nulla vestibulum porttitor sed ut orci. Nulla facilisi. In et mauris eget justo pulvinar cursus vel at ligula. Maecenas rhoncus turpis a eros lobortis ultrices. Nulla vel tellus leo. Nunc vitae sodales mi, non interdum tortor. Aliquam erat volutpat.

    27 | 28 |

    Dapibus ut vehicula vitae

    29 | 30 |

    Interdum et malesuada fames ac ante ipsum primis in faucibus. Integer a felis non augue fringilla condimentum. Mauris at semper ex. Aliquam magna dolor, gravida ut risus id, malesuada fringilla ligula. Nunc nec magna non augue elementum porta a eu arcu. Donec pellentesque dapibus neque, sit amet pulvinar nisl mollis nec. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi faucibus lacinia tellus tincidunt sagittis. Ut feugiat massa eu viverra varius. Nunc eget magna eu dolor sagittis sodales.

    31 | 32 |

    Proin feugiat massa id fermentum molestie. Nam eu congue ante, eget molestie mi. In commodo orci a augue pharetra, eget sollicitudin diam scelerisque. Suspendisse cursus elit sit amet metus faucibus, non vulputate nibh ornare. Duis fermentum efficitur eros, vel pellentesque leo bibendum eget. Interdum et malesuada fames ac ante ipsum primis in faucibus. Ut vel nisi nec lorem ullamcorper molestie id auctor leo. Nunc facilisis dignissim nisi, sit amet interdum neque egestas sed. Aliquam erat dui, rutrum sed orci at, laoreet blandit diam. Donec commodo lacus leo, et rhoncus eros laoreet fermentum. Mauris sit amet tempor neque, ac lacinia ipsum. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Fusce vitae consequat velit. Mauris mollis leo quis ullamcorper sollicitudin. Proin non justo at leo iaculis bibendum.

    33 | 34 |

    Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In vitae eros sit amet orci malesuada elementum vitae quis leo. Maecenas turpis metus, viverra id tempor in, vehicula vel eros. Etiam finibus a nisl a scelerisque. Fusce ut auctor urna, ac congue eros. Sed eu finibus libero, commodo eleifend enim. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Proin tristique erat eu erat suscipit, a venenatis lorem vehicula. Nulla dapibus rhoncus tincidunt. Etiam iaculis sem mi, a suscipit massa interdum et. Aliquam ultricies dolor et semper maximus. Donec egestas faucibus metus, quis placerat nisl blandit sed. Morbi porttitor elit scelerisque, vestibulum neque ac, porttitor sem. Ut vel ligula consectetur, pharetra nibh at, sollicitudin erat.

    35 | 36 |

    Sed luctus

    37 | 38 |

    Metus eget laoreet feugiat, felis sem faucibus metus, quis cursus ante ex ut nisi. Etiam id hendrerit felis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nunc commodo cursus lacus at congue. Morbi laoreet iaculis quam non iaculis. Etiam volutpat auctor risus in accumsan. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Maecenas sed ultrices dolor, eu rutrum ex. Suspendisse tincidunt metus metus, ut egestas nisi efficitur non. Curabitur consequat dictum dolor nec auctor. Vestibulum id felis ac justo congue rhoncus. Phasellus pharetra elit id aliquet aliquet. Vestibulum elementum lorem arcu, eu tincidunt odio euismod consectetur. Sed porta dolor orci, a pellentesque nibh elementum sed. Proin ligula ante, aliquam sit amet magna eget, dignissim scelerisque ante. Nulla vulputate tortor ut mi dapibus, a iaculis nulla facilisis.

    39 | 40 |

    Ut dignissim lobortis vestibulum

    41 | 42 |

    Integer a libero vitae nisi vulputate convallis. Mauris in metus non tortor bibendum scelerisque at quis ante. Nulla dui diam, faucibus sit amet ullamcorper ac, lacinia porta neque. Nulla et urna non sem porta bibendum eu ac tortor. Fusce id metus sem. Vivamus nisl urna, vulputate quis arcu ut, auctor iaculis lacus. Aliquam sed dui sed nulla interdum laoreet non at dolor. Duis laoreet sem justo, vulputate ullamcorper diam pretium ac. Sed convallis ante sit amet metus lacinia, sit amet egestas nunc tempor. Sed eget mi tempor, semper libero porttitor, facilisis leo. Aenean vel imperdiet tellus. Aenean venenatis erat sed odio finibus, at vehicula lorem aliquam. Praesent porttitor velit interdum nunc mollis gravida at non nunc. Proin vitae vulputate elit, vitae commodo tellus.

    43 | 44 |

    Nullam sodales orci

    45 | 46 |

    Pellentesque eu sem mauris. Quisque condimentum nisi a magna tempor dictum. Vestibulum placerat maximus ligula, vitae ultrices nibh aliquam non. In consequat elementum nunc ut elementum. Donec maximus condimentum metus id porta. Sed id congue justo, ac lacinia ex. Nunc convallis imperdiet sem eget euismod. Aenean tristique ex massa, sed ultrices est varius eget. Vivamus vitae metus vitae nisi ornare vehicula.

    47 | 48 |

    Curabitur posuere, massa vel gravida hendrerit, turpis ligula tincidunt ligula, vitae consequat quam risus ac lorem. Aenean lacinia vitae diam sed lobortis. Vivamus vestibulum quam dui, facilisis tempor ante pellentesque sit amet. Proin quis arcu commodo, dapibus tortor id, euismod ex. Mauris sit amet libero et felis euismod pulvinar. Donec tempor tincidunt elementum.

    49 | 50 |

    Nulla placerat urna laoreet

    51 | 52 |

    Pharetra massa vitae, lacinia lectus. Pellentesque tristique purus velit, non malesuada lectus ullamcorper non. Donec sed molestie libero, vel pharetra arcu. Fusce efficitur nisi ex, eu bibendum massa laoreet eu. Proin malesuada condimentum quam. Curabitur in cursus justo. Duis suscipit vulputate ante quis commodo. Sed tincidunt rhoncus tortor, sed vestibulum nisi vulputate nec. Maecenas hendrerit, ipsum vitae egestas suscipit, lorem nibh dignissim lorem, ac placerat mi est eu risus. Vestibulum nisl nisl, auctor nec ante feugiat, maximus pretium mi.

    53 | 54 |

    Donec a malesuada nibh, in cursus eros. Pellentesque sit amet accumsan justo. Donec ut quam lobortis, sagittis erat eu, finibus leo. Proin vestibulum ex dui, id ultrices ligula vulputate quis. Suspendisse est sem, hendrerit sed augue quis, tempus iaculis metus. Etiam rutrum gravida interdum. Duis vitae auctor nunc. Praesent ac metus massa. In ligula sapien, aliquam consequat tincidunt a, iaculis at enim. Nulla facilisi. Cras vulputate ante purus, vitae volutpat ipsum dignissim sed. Mauris dapibus nisl tincidunt dolor blandit, sit amet ultricies neque bibendum. Sed tempor nunc id odio iaculis porta. Pellentesque ut scelerisque mauris, eu bibendum eros.

    55 | 56 |

    Morbi dignissim aliquet nunc eu dignissim. Nulla ultrices felis elit, a posuere massa tincidunt nec. Nulla facilisi. Maecenas lorem nisi, lobortis eu dolor vitae, pretium dapibus ante. Aenean lacus ex, fringilla venenatis tempus sit amet, mollis id tortor. Duis massa mauris, aliquam a lacus a, elementum finibus sem. Praesent elementum tortor est, ut condimentum justo ullamcorper nec. Quisque quis ligula eu ex venenatis sagittis in non dui.

    57 | 58 |

    Fusce mattis sagittis

    59 | 60 |

    Etiam condimentum, risus eu hendrerit mattis, ipsum nisi aliquam ipsum, a venenatis purus ipsum sit amet lorem. Maecenas tempus dolor nec elit pharetra semper. Nullam id nibh blandit, viverra sem non, porttitor est. Vestibulum ac sollicitudin ligula. Aliquam erat volutpat. Donec id neque a erat sagittis tempus. Duis feugiat velit eget ligula egestas suscipit. Cras pharetra auctor vestibulum. Nullam et lorem eu mi fermentum dignissim. Sed interdum enim ac sem faucibus, non faucibus est placerat. Sed pharetra lorem sit amet augue feugiat auctor.

    61 |
    62 |
    63 | 64 | 65 | -------------------------------------------------------------------------------- /examples/plain/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 15 | 16 | 17 |
    18 | 19 |
    20 |

    Pellentesque habitant

    21 | 22 |

    Morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus

    23 | 24 |

    Etiam dignissim interdum

    25 | 26 |

    Mauris sed diam vel nulla vestibulum porttitor sed ut orci. Nulla facilisi. In et mauris eget justo pulvinar cursus vel at ligula. Maecenas rhoncus turpis a eros lobortis ultrices. Nulla vel tellus leo. Nunc vitae sodales mi, non interdum tortor. Aliquam erat volutpat.

    27 | 28 |

    Dapibus ut vehicula vitae

    29 | 30 |

    Interdum et malesuada fames ac ante ipsum primis in faucibus. Integer a felis non augue fringilla condimentum. Mauris at semper ex. Aliquam magna dolor, gravida ut risus id, malesuada fringilla ligula. Nunc nec magna non augue elementum porta a eu arcu. Donec pellentesque dapibus neque, sit amet pulvinar nisl mollis nec. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi faucibus lacinia tellus tincidunt sagittis. Ut feugiat massa eu viverra varius. Nunc eget magna eu dolor sagittis sodales.

    31 | 32 |

    Proin feugiat massa id fermentum molestie. Nam eu congue ante, eget molestie mi. In commodo orci a augue pharetra, eget sollicitudin diam scelerisque. Suspendisse cursus elit sit amet metus faucibus, non vulputate nibh ornare. Duis fermentum efficitur eros, vel pellentesque leo bibendum eget. Interdum et malesuada fames ac ante ipsum primis in faucibus. Ut vel nisi nec lorem ullamcorper molestie id auctor leo. Nunc facilisis dignissim nisi, sit amet interdum neque egestas sed. Aliquam erat dui, rutrum sed orci at, laoreet blandit diam. Donec commodo lacus leo, et rhoncus eros laoreet fermentum. Mauris sit amet tempor neque, ac lacinia ipsum. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Fusce vitae consequat velit. Mauris mollis leo quis ullamcorper sollicitudin. Proin non justo at leo iaculis bibendum.

    33 | 34 |

    Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In vitae eros sit amet orci malesuada elementum vitae quis leo. Maecenas turpis metus, viverra id tempor in, vehicula vel eros. Etiam finibus a nisl a scelerisque. Fusce ut auctor urna, ac congue eros. Sed eu finibus libero, commodo eleifend enim. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Proin tristique erat eu erat suscipit, a venenatis lorem vehicula. Nulla dapibus rhoncus tincidunt. Etiam iaculis sem mi, a suscipit massa interdum et. Aliquam ultricies dolor et semper maximus. Donec egestas faucibus metus, quis placerat nisl blandit sed. Morbi porttitor elit scelerisque, vestibulum neque ac, porttitor sem. Ut vel ligula consectetur, pharetra nibh at, sollicitudin erat.

    35 | 36 |

    Sed luctus

    37 | 38 |

    Metus eget laoreet feugiat, felis sem faucibus metus, quis cursus ante ex ut nisi. Etiam id hendrerit felis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nunc commodo cursus lacus at congue. Morbi laoreet iaculis quam non iaculis. Etiam volutpat auctor risus in accumsan. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Maecenas sed ultrices dolor, eu rutrum ex. Suspendisse tincidunt metus metus, ut egestas nisi efficitur non. Curabitur consequat dictum dolor nec auctor. Vestibulum id felis ac justo congue rhoncus. Phasellus pharetra elit id aliquet aliquet. Vestibulum elementum lorem arcu, eu tincidunt odio euismod consectetur. Sed porta dolor orci, a pellentesque nibh elementum sed. Proin ligula ante, aliquam sit amet magna eget, dignissim scelerisque ante. Nulla vulputate tortor ut mi dapibus, a iaculis nulla facilisis.

    39 | 40 |

    Ut dignissim lobortis vestibulum

    41 | 42 |

    Integer a libero vitae nisi vulputate convallis. Mauris in metus non tortor bibendum scelerisque at quis ante. Nulla dui diam, faucibus sit amet ullamcorper ac, lacinia porta neque. Nulla et urna non sem porta bibendum eu ac tortor. Fusce id metus sem. Vivamus nisl urna, vulputate quis arcu ut, auctor iaculis lacus. Aliquam sed dui sed nulla interdum laoreet non at dolor. Duis laoreet sem justo, vulputate ullamcorper diam pretium ac. Sed convallis ante sit amet metus lacinia, sit amet egestas nunc tempor. Sed eget mi tempor, semper libero porttitor, facilisis leo. Aenean vel imperdiet tellus. Aenean venenatis erat sed odio finibus, at vehicula lorem aliquam. Praesent porttitor velit interdum nunc mollis gravida at non nunc. Proin vitae vulputate elit, vitae commodo tellus.

    43 | 44 |

    Nullam sodales orci

    45 | 46 |

    Pellentesque eu sem mauris. Quisque condimentum nisi a magna tempor dictum. Vestibulum placerat maximus ligula, vitae ultrices nibh aliquam non. In consequat elementum nunc ut elementum. Donec maximus condimentum metus id porta. Sed id congue justo, ac lacinia ex. Nunc convallis imperdiet sem eget euismod. Aenean tristique ex massa, sed ultrices est varius eget. Vivamus vitae metus vitae nisi ornare vehicula.

    47 | 48 |

    Curabitur posuere, massa vel gravida hendrerit, turpis ligula tincidunt ligula, vitae consequat quam risus ac lorem. Aenean lacinia vitae diam sed lobortis. Vivamus vestibulum quam dui, facilisis tempor ante pellentesque sit amet. Proin quis arcu commodo, dapibus tortor id, euismod ex. Mauris sit amet libero et felis euismod pulvinar. Donec tempor tincidunt elementum.

    49 | 50 |

    Nulla placerat urna laoreet

    51 | 52 |

    Pharetra massa vitae, lacinia lectus. Pellentesque tristique purus velit, non malesuada lectus ullamcorper non. Donec sed molestie libero, vel pharetra arcu. Fusce efficitur nisi ex, eu bibendum massa laoreet eu. Proin malesuada condimentum quam. Curabitur in cursus justo. Duis suscipit vulputate ante quis commodo. Sed tincidunt rhoncus tortor, sed vestibulum nisi vulputate nec. Maecenas hendrerit, ipsum vitae egestas suscipit, lorem nibh dignissim lorem, ac placerat mi est eu risus. Vestibulum nisl nisl, auctor nec ante feugiat, maximus pretium mi.

    53 | 54 |

    Donec a malesuada nibh, in cursus eros. Pellentesque sit amet accumsan justo. Donec ut quam lobortis, sagittis erat eu, finibus leo. Proin vestibulum ex dui, id ultrices ligula vulputate quis. Suspendisse est sem, hendrerit sed augue quis, tempus iaculis metus. Etiam rutrum gravida interdum. Duis vitae auctor nunc. Praesent ac metus massa. In ligula sapien, aliquam consequat tincidunt a, iaculis at enim. Nulla facilisi. Cras vulputate ante purus, vitae volutpat ipsum dignissim sed. Mauris dapibus nisl tincidunt dolor blandit, sit amet ultricies neque bibendum. Sed tempor nunc id odio iaculis porta. Pellentesque ut scelerisque mauris, eu bibendum eros.

    55 | 56 |

    Morbi dignissim aliquet nunc eu dignissim. Nulla ultrices felis elit, a posuere massa tincidunt nec. Nulla facilisi. Maecenas lorem nisi, lobortis eu dolor vitae, pretium dapibus ante. Aenean lacus ex, fringilla venenatis tempus sit amet, mollis id tortor. Duis massa mauris, aliquam a lacus a, elementum finibus sem. Praesent elementum tortor est, ut condimentum justo ullamcorper nec. Quisque quis ligula eu ex venenatis sagittis in non dui.

    57 | 58 |

    Fusce mattis sagittis

    59 | 60 |

    Etiam condimentum, risus eu hendrerit mattis, ipsum nisi aliquam ipsum, a venenatis purus ipsum sit amet lorem. Maecenas tempus dolor nec elit pharetra semper. Nullam id nibh blandit, viverra sem non, porttitor est. Vestibulum ac sollicitudin ligula. Aliquam erat volutpat. Donec id neque a erat sagittis tempus. Duis feugiat velit eget ligula egestas suscipit. Cras pharetra auctor vestibulum. Nullam et lorem eu mi fermentum dignissim. Sed interdum enim ac sem faucibus, non faucibus est placerat. Sed pharetra lorem sit amet augue feugiat auctor.

    61 |
    62 |
    63 | 64 | 65 | -------------------------------------------------------------------------------- /examples/list-element/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 16 | 17 | 18 |
    19 |
    20 |

    Pellentesque habitant

    21 | 22 |

    Morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus

    23 | 24 |

    Etiam dignissim interdum

    25 | 26 |

    Mauris sed diam vel nulla vestibulum porttitor sed ut orci. Nulla facilisi. In et mauris eget justo pulvinar cursus vel at ligula. Maecenas rhoncus turpis a eros lobortis ultrices. Nulla vel tellus leo. Nunc vitae sodales mi, non interdum tortor. Aliquam erat volutpat.

    27 | 28 |

    Dapibus ut vehicula vitae

    29 | 30 |

    Interdum et malesuada fames ac ante ipsum primis in faucibus. Integer a felis non augue fringilla condimentum. Mauris at semper ex. Aliquam magna dolor, gravida ut risus id, malesuada fringilla ligula. Nunc nec magna non augue elementum porta a eu arcu. Donec pellentesque dapibus neque, sit amet pulvinar nisl mollis nec. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi faucibus lacinia tellus tincidunt sagittis. Ut feugiat massa eu viverra varius. Nunc eget magna eu dolor sagittis sodales.

    31 | 32 |

    Proin feugiat massa id fermentum molestie. Nam eu congue ante, eget molestie mi. In commodo orci a augue pharetra, eget sollicitudin diam scelerisque. Suspendisse cursus elit sit amet metus faucibus, non vulputate nibh ornare. Duis fermentum efficitur eros, vel pellentesque leo bibendum eget. Interdum et malesuada fames ac ante ipsum primis in faucibus. Ut vel nisi nec lorem ullamcorper molestie id auctor leo. Nunc facilisis dignissim nisi, sit amet interdum neque egestas sed. Aliquam erat dui, rutrum sed orci at, laoreet blandit diam. Donec commodo lacus leo, et rhoncus eros laoreet fermentum. Mauris sit amet tempor neque, ac lacinia ipsum. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Fusce vitae consequat velit. Mauris mollis leo quis ullamcorper sollicitudin. Proin non justo at leo iaculis bibendum.

    33 | 34 |

    Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In vitae eros sit amet orci malesuada elementum vitae quis leo. Maecenas turpis metus, viverra id tempor in, vehicula vel eros. Etiam finibus a nisl a scelerisque. Fusce ut auctor urna, ac congue eros. Sed eu finibus libero, commodo eleifend enim. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Proin tristique erat eu erat suscipit, a venenatis lorem vehicula. Nulla dapibus rhoncus tincidunt. Etiam iaculis sem mi, a suscipit massa interdum et. Aliquam ultricies dolor et semper maximus. Donec egestas faucibus metus, quis placerat nisl blandit sed. Morbi porttitor elit scelerisque, vestibulum neque ac, porttitor sem. Ut vel ligula consectetur, pharetra nibh at, sollicitudin erat.

    35 | 36 |

    Sed luctus

    37 | 38 |

    Metus eget laoreet feugiat, felis sem faucibus metus, quis cursus ante ex ut nisi. Etiam id hendrerit felis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nunc commodo cursus lacus at congue. Morbi laoreet iaculis quam non iaculis. Etiam volutpat auctor risus in accumsan. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Maecenas sed ultrices dolor, eu rutrum ex. Suspendisse tincidunt metus metus, ut egestas nisi efficitur non. Curabitur consequat dictum dolor nec auctor. Vestibulum id felis ac justo congue rhoncus. Phasellus pharetra elit id aliquet aliquet. Vestibulum elementum lorem arcu, eu tincidunt odio euismod consectetur. Sed porta dolor orci, a pellentesque nibh elementum sed. Proin ligula ante, aliquam sit amet magna eget, dignissim scelerisque ante. Nulla vulputate tortor ut mi dapibus, a iaculis nulla facilisis.

    39 | 40 |

    Ut dignissim lobortis vestibulum

    41 | 42 |

    Integer a libero vitae nisi vulputate convallis. Mauris in metus non tortor bibendum scelerisque at quis ante. Nulla dui diam, faucibus sit amet ullamcorper ac, lacinia porta neque. Nulla et urna non sem porta bibendum eu ac tortor. Fusce id metus sem. Vivamus nisl urna, vulputate quis arcu ut, auctor iaculis lacus. Aliquam sed dui sed nulla interdum laoreet non at dolor. Duis laoreet sem justo, vulputate ullamcorper diam pretium ac. Sed convallis ante sit amet metus lacinia, sit amet egestas nunc tempor. Sed eget mi tempor, semper libero porttitor, facilisis leo. Aenean vel imperdiet tellus. Aenean venenatis erat sed odio finibus, at vehicula lorem aliquam. Praesent porttitor velit interdum nunc mollis gravida at non nunc. Proin vitae vulputate elit, vitae commodo tellus.

    43 | 44 |

    Nullam sodales orci

    45 | 46 |

    Pellentesque eu sem mauris. Quisque condimentum nisi a magna tempor dictum. Vestibulum placerat maximus ligula, vitae ultrices nibh aliquam non. In consequat elementum nunc ut elementum. Donec maximus condimentum metus id porta. Sed id congue justo, ac lacinia ex. Nunc convallis imperdiet sem eget euismod. Aenean tristique ex massa, sed ultrices est varius eget. Vivamus vitae metus vitae nisi ornare vehicula.

    47 | 48 |

    Curabitur posuere, massa vel gravida hendrerit, turpis ligula tincidunt ligula, vitae consequat quam risus ac lorem. Aenean lacinia vitae diam sed lobortis. Vivamus vestibulum quam dui, facilisis tempor ante pellentesque sit amet. Proin quis arcu commodo, dapibus tortor id, euismod ex. Mauris sit amet libero et felis euismod pulvinar. Donec tempor tincidunt elementum.

    49 | 50 |

    Nulla placerat urna laoreet

    51 | 52 |

    Pharetra massa vitae, lacinia lectus. Pellentesque tristique purus velit, non malesuada lectus ullamcorper non. Donec sed molestie libero, vel pharetra arcu. Fusce efficitur nisi ex, eu bibendum massa laoreet eu. Proin malesuada condimentum quam. Curabitur in cursus justo. Duis suscipit vulputate ante quis commodo. Sed tincidunt rhoncus tortor, sed vestibulum nisi vulputate nec. Maecenas hendrerit, ipsum vitae egestas suscipit, lorem nibh dignissim lorem, ac placerat mi est eu risus. Vestibulum nisl nisl, auctor nec ante feugiat, maximus pretium mi.

    53 | 54 |

    Donec a malesuada nibh, in cursus eros. Pellentesque sit amet accumsan justo. Donec ut quam lobortis, sagittis erat eu, finibus leo. Proin vestibulum ex dui, id ultrices ligula vulputate quis. Suspendisse est sem, hendrerit sed augue quis, tempus iaculis metus. Etiam rutrum gravida interdum. Duis vitae auctor nunc. Praesent ac metus massa. In ligula sapien, aliquam consequat tincidunt a, iaculis at enim. Nulla facilisi. Cras vulputate ante purus, vitae volutpat ipsum dignissim sed. Mauris dapibus nisl tincidunt dolor blandit, sit amet ultricies neque bibendum. Sed tempor nunc id odio iaculis porta. Pellentesque ut scelerisque mauris, eu bibendum eros.

    55 | 56 |

    Morbi dignissim aliquet nunc eu dignissim. Nulla ultrices felis elit, a posuere massa tincidunt nec. Nulla facilisi. Maecenas lorem nisi, lobortis eu dolor vitae, pretium dapibus ante. Aenean lacus ex, fringilla venenatis tempus sit amet, mollis id tortor. Duis massa mauris, aliquam a lacus a, elementum finibus sem. Praesent elementum tortor est, ut condimentum justo ullamcorper nec. Quisque quis ligula eu ex venenatis sagittis in non dui.

    57 | 58 |

    Fusce mattis sagittis

    59 | 60 |

    Etiam condimentum, risus eu hendrerit mattis, ipsum nisi aliquam ipsum, a venenatis purus ipsum sit amet lorem. Maecenas tempus dolor nec elit pharetra semper. Nullam id nibh blandit, viverra sem non, porttitor est. Vestibulum ac sollicitudin ligula. Aliquam erat volutpat. Donec id neque a erat sagittis tempus. Duis feugiat velit eget ligula egestas suscipit. Cras pharetra auctor vestibulum. Nullam et lorem eu mi fermentum dignissim. Sed interdum enim ac sem faucibus, non faucibus est placerat. Sed pharetra lorem sit amet augue feugiat auctor.

    61 |
    62 |
    63 | 64 | -------------------------------------------------------------------------------- /examples/smooth-scrolling/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 22 | 23 | 24 |
    25 | 26 |
    27 |

    Pellentesque habitant

    28 | 29 |

    Morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus

    30 | 31 |

    Etiam dignissim interdum

    32 | 33 |

    Mauris sed diam vel nulla vestibulum porttitor sed ut orci. Nulla facilisi. In et mauris eget justo pulvinar cursus vel at ligula. Maecenas rhoncus turpis a eros lobortis ultrices. Nulla vel tellus leo. Nunc vitae sodales mi, non interdum tortor. Aliquam erat volutpat.

    34 | 35 |

    Dapibus ut vehicula vitae

    36 | 37 |

    Interdum et malesuada fames ac ante ipsum primis in faucibus. Integer a felis non augue fringilla condimentum. Mauris at semper ex. Aliquam magna dolor, gravida ut risus id, malesuada fringilla ligula. Nunc nec magna non augue elementum porta a eu arcu. Donec pellentesque dapibus neque, sit amet pulvinar nisl mollis nec. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi faucibus lacinia tellus tincidunt sagittis. Ut feugiat massa eu viverra varius. Nunc eget magna eu dolor sagittis sodales.

    38 | 39 |

    Proin feugiat massa id fermentum molestie. Nam eu congue ante, eget molestie mi. In commodo orci a augue pharetra, eget sollicitudin diam scelerisque. Suspendisse cursus elit sit amet metus faucibus, non vulputate nibh ornare. Duis fermentum efficitur eros, vel pellentesque leo bibendum eget. Interdum et malesuada fames ac ante ipsum primis in faucibus. Ut vel nisi nec lorem ullamcorper molestie id auctor leo. Nunc facilisis dignissim nisi, sit amet interdum neque egestas sed. Aliquam erat dui, rutrum sed orci at, laoreet blandit diam. Donec commodo lacus leo, et rhoncus eros laoreet fermentum. Mauris sit amet tempor neque, ac lacinia ipsum. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Fusce vitae consequat velit. Mauris mollis leo quis ullamcorper sollicitudin. Proin non justo at leo iaculis bibendum.

    40 | 41 |

    Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In vitae eros sit amet orci malesuada elementum vitae quis leo. Maecenas turpis metus, viverra id tempor in, vehicula vel eros. Etiam finibus a nisl a scelerisque. Fusce ut auctor urna, ac congue eros. Sed eu finibus libero, commodo eleifend enim. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Proin tristique erat eu erat suscipit, a venenatis lorem vehicula. Nulla dapibus rhoncus tincidunt. Etiam iaculis sem mi, a suscipit massa interdum et. Aliquam ultricies dolor et semper maximus. Donec egestas faucibus metus, quis placerat nisl blandit sed. Morbi porttitor elit scelerisque, vestibulum neque ac, porttitor sem. Ut vel ligula consectetur, pharetra nibh at, sollicitudin erat.

    42 | 43 |

    Sed luctus

    44 | 45 |

    Metus eget laoreet feugiat, felis sem faucibus metus, quis cursus ante ex ut nisi. Etiam id hendrerit felis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nunc commodo cursus lacus at congue. Morbi laoreet iaculis quam non iaculis. Etiam volutpat auctor risus in accumsan. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Maecenas sed ultrices dolor, eu rutrum ex. Suspendisse tincidunt metus metus, ut egestas nisi efficitur non. Curabitur consequat dictum dolor nec auctor. Vestibulum id felis ac justo congue rhoncus. Phasellus pharetra elit id aliquet aliquet. Vestibulum elementum lorem arcu, eu tincidunt odio euismod consectetur. Sed porta dolor orci, a pellentesque nibh elementum sed. Proin ligula ante, aliquam sit amet magna eget, dignissim scelerisque ante. Nulla vulputate tortor ut mi dapibus, a iaculis nulla facilisis.

    46 | 47 |

    Ut dignissim lobortis vestibulum

    48 | 49 |

    Integer a libero vitae nisi vulputate convallis. Mauris in metus non tortor bibendum scelerisque at quis ante. Nulla dui diam, faucibus sit amet ullamcorper ac, lacinia porta neque. Nulla et urna non sem porta bibendum eu ac tortor. Fusce id metus sem. Vivamus nisl urna, vulputate quis arcu ut, auctor iaculis lacus. Aliquam sed dui sed nulla interdum laoreet non at dolor. Duis laoreet sem justo, vulputate ullamcorper diam pretium ac. Sed convallis ante sit amet metus lacinia, sit amet egestas nunc tempor. Sed eget mi tempor, semper libero porttitor, facilisis leo. Aenean vel imperdiet tellus. Aenean venenatis erat sed odio finibus, at vehicula lorem aliquam. Praesent porttitor velit interdum nunc mollis gravida at non nunc. Proin vitae vulputate elit, vitae commodo tellus.

    50 | 51 |

    Nullam sodales orci

    52 | 53 |

    Pellentesque eu sem mauris. Quisque condimentum nisi a magna tempor dictum. Vestibulum placerat maximus ligula, vitae ultrices nibh aliquam non. In consequat elementum nunc ut elementum. Donec maximus condimentum metus id porta. Sed id congue justo, ac lacinia ex. Nunc convallis imperdiet sem eget euismod. Aenean tristique ex massa, sed ultrices est varius eget. Vivamus vitae metus vitae nisi ornare vehicula.

    54 | 55 |

    Curabitur posuere, massa vel gravida hendrerit, turpis ligula tincidunt ligula, vitae consequat quam risus ac lorem. Aenean lacinia vitae diam sed lobortis. Vivamus vestibulum quam dui, facilisis tempor ante pellentesque sit amet. Proin quis arcu commodo, dapibus tortor id, euismod ex. Mauris sit amet libero et felis euismod pulvinar. Donec tempor tincidunt elementum.

    56 | 57 |

    Nulla placerat urna laoreet

    58 | 59 |

    Pharetra massa vitae, lacinia lectus. Pellentesque tristique purus velit, non malesuada lectus ullamcorper non. Donec sed molestie libero, vel pharetra arcu. Fusce efficitur nisi ex, eu bibendum massa laoreet eu. Proin malesuada condimentum quam. Curabitur in cursus justo. Duis suscipit vulputate ante quis commodo. Sed tincidunt rhoncus tortor, sed vestibulum nisi vulputate nec. Maecenas hendrerit, ipsum vitae egestas suscipit, lorem nibh dignissim lorem, ac placerat mi est eu risus. Vestibulum nisl nisl, auctor nec ante feugiat, maximus pretium mi.

    60 | 61 |

    Donec a malesuada nibh, in cursus eros. Pellentesque sit amet accumsan justo. Donec ut quam lobortis, sagittis erat eu, finibus leo. Proin vestibulum ex dui, id ultrices ligula vulputate quis. Suspendisse est sem, hendrerit sed augue quis, tempus iaculis metus. Etiam rutrum gravida interdum. Duis vitae auctor nunc. Praesent ac metus massa. In ligula sapien, aliquam consequat tincidunt a, iaculis at enim. Nulla facilisi. Cras vulputate ante purus, vitae volutpat ipsum dignissim sed. Mauris dapibus nisl tincidunt dolor blandit, sit amet ultricies neque bibendum. Sed tempor nunc id odio iaculis porta. Pellentesque ut scelerisque mauris, eu bibendum eros.

    62 | 63 |

    Morbi dignissim aliquet nunc eu dignissim. Nulla ultrices felis elit, a posuere massa tincidunt nec. Nulla facilisi. Maecenas lorem nisi, lobortis eu dolor vitae, pretium dapibus ante. Aenean lacus ex, fringilla venenatis tempus sit amet, mollis id tortor. Duis massa mauris, aliquam a lacus a, elementum finibus sem. Praesent elementum tortor est, ut condimentum justo ullamcorper nec. Quisque quis ligula eu ex venenatis sagittis in non dui.

    64 | 65 |

    Fusce mattis sagittis

    66 | 67 |

    Etiam condimentum, risus eu hendrerit mattis, ipsum nisi aliquam ipsum, a venenatis purus ipsum sit amet lorem. Maecenas tempus dolor nec elit pharetra semper. Nullam id nibh blandit, viverra sem non, porttitor est. Vestibulum ac sollicitudin ligula. Aliquam erat volutpat. Donec id neque a erat sagittis tempus. Duis feugiat velit eget ligula egestas suscipit. Cras pharetra auctor vestibulum. Nullam et lorem eu mi fermentum dignissim. Sed interdum enim ac sem faucibus, non faucibus est placerat. Sed pharetra lorem sit amet augue feugiat auctor.

    68 |
    69 |
    70 | 71 | 72 | -------------------------------------------------------------------------------- /examples/events/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 28 | 29 | 30 |
    31 | 32 |
    33 |

    Pellentesque habitant

    34 | 35 |

    Morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus

    36 | 37 |

    Etiam dignissim interdum

    38 | 39 |

    Mauris sed diam vel nulla vestibulum porttitor sed ut orci. Nulla facilisi. In et mauris eget justo pulvinar cursus vel at ligula. Maecenas rhoncus turpis a eros lobortis ultrices. Nulla vel tellus leo. Nunc vitae sodales mi, non interdum tortor. Aliquam erat volutpat.

    40 | 41 |

    Dapibus ut vehicula vitae

    42 | 43 |

    Interdum et malesuada fames ac ante ipsum primis in faucibus. Integer a felis non augue fringilla condimentum. Mauris at semper ex. Aliquam magna dolor, gravida ut risus id, malesuada fringilla ligula. Nunc nec magna non augue elementum porta a eu arcu. Donec pellentesque dapibus neque, sit amet pulvinar nisl mollis nec. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi faucibus lacinia tellus tincidunt sagittis. Ut feugiat massa eu viverra varius. Nunc eget magna eu dolor sagittis sodales.

    44 | 45 |

    Proin feugiat massa id fermentum molestie. Nam eu congue ante, eget molestie mi. In commodo orci a augue pharetra, eget sollicitudin diam scelerisque. Suspendisse cursus elit sit amet metus faucibus, non vulputate nibh ornare. Duis fermentum efficitur eros, vel pellentesque leo bibendum eget. Interdum et malesuada fames ac ante ipsum primis in faucibus. Ut vel nisi nec lorem ullamcorper molestie id auctor leo. Nunc facilisis dignissim nisi, sit amet interdum neque egestas sed. Aliquam erat dui, rutrum sed orci at, laoreet blandit diam. Donec commodo lacus leo, et rhoncus eros laoreet fermentum. Mauris sit amet tempor neque, ac lacinia ipsum. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Fusce vitae consequat velit. Mauris mollis leo quis ullamcorper sollicitudin. Proin non justo at leo iaculis bibendum.

    46 | 47 |

    Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In vitae eros sit amet orci malesuada elementum vitae quis leo. Maecenas turpis metus, viverra id tempor in, vehicula vel eros. Etiam finibus a nisl a scelerisque. Fusce ut auctor urna, ac congue eros. Sed eu finibus libero, commodo eleifend enim. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Proin tristique erat eu erat suscipit, a venenatis lorem vehicula. Nulla dapibus rhoncus tincidunt. Etiam iaculis sem mi, a suscipit massa interdum et. Aliquam ultricies dolor et semper maximus. Donec egestas faucibus metus, quis placerat nisl blandit sed. Morbi porttitor elit scelerisque, vestibulum neque ac, porttitor sem. Ut vel ligula consectetur, pharetra nibh at, sollicitudin erat.

    48 | 49 |

    Sed luctus

    50 | 51 |

    Metus eget laoreet feugiat, felis sem faucibus metus, quis cursus ante ex ut nisi. Etiam id hendrerit felis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nunc commodo cursus lacus at congue. Morbi laoreet iaculis quam non iaculis. Etiam volutpat auctor risus in accumsan. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Maecenas sed ultrices dolor, eu rutrum ex. Suspendisse tincidunt metus metus, ut egestas nisi efficitur non. Curabitur consequat dictum dolor nec auctor. Vestibulum id felis ac justo congue rhoncus. Phasellus pharetra elit id aliquet aliquet. Vestibulum elementum lorem arcu, eu tincidunt odio euismod consectetur. Sed porta dolor orci, a pellentesque nibh elementum sed. Proin ligula ante, aliquam sit amet magna eget, dignissim scelerisque ante. Nulla vulputate tortor ut mi dapibus, a iaculis nulla facilisis.

    52 | 53 |

    Ut dignissim lobortis vestibulum

    54 | 55 |

    Integer a libero vitae nisi vulputate convallis. Mauris in metus non tortor bibendum scelerisque at quis ante. Nulla dui diam, faucibus sit amet ullamcorper ac, lacinia porta neque. Nulla et urna non sem porta bibendum eu ac tortor. Fusce id metus sem. Vivamus nisl urna, vulputate quis arcu ut, auctor iaculis lacus. Aliquam sed dui sed nulla interdum laoreet non at dolor. Duis laoreet sem justo, vulputate ullamcorper diam pretium ac. Sed convallis ante sit amet metus lacinia, sit amet egestas nunc tempor. Sed eget mi tempor, semper libero porttitor, facilisis leo. Aenean vel imperdiet tellus. Aenean venenatis erat sed odio finibus, at vehicula lorem aliquam. Praesent porttitor velit interdum nunc mollis gravida at non nunc. Proin vitae vulputate elit, vitae commodo tellus.

    56 | 57 |

    Nullam sodales orci

    58 | 59 |

    Pellentesque eu sem mauris. Quisque condimentum nisi a magna tempor dictum. Vestibulum placerat maximus ligula, vitae ultrices nibh aliquam non. In consequat elementum nunc ut elementum. Donec maximus condimentum metus id porta. Sed id congue justo, ac lacinia ex. Nunc convallis imperdiet sem eget euismod. Aenean tristique ex massa, sed ultrices est varius eget. Vivamus vitae metus vitae nisi ornare vehicula.

    60 | 61 |

    Curabitur posuere, massa vel gravida hendrerit, turpis ligula tincidunt ligula, vitae consequat quam risus ac lorem. Aenean lacinia vitae diam sed lobortis. Vivamus vestibulum quam dui, facilisis tempor ante pellentesque sit amet. Proin quis arcu commodo, dapibus tortor id, euismod ex. Mauris sit amet libero et felis euismod pulvinar. Donec tempor tincidunt elementum.

    62 | 63 |

    Nulla placerat urna laoreet

    64 | 65 |

    Pharetra massa vitae, lacinia lectus. Pellentesque tristique purus velit, non malesuada lectus ullamcorper non. Donec sed molestie libero, vel pharetra arcu. Fusce efficitur nisi ex, eu bibendum massa laoreet eu. Proin malesuada condimentum quam. Curabitur in cursus justo. Duis suscipit vulputate ante quis commodo. Sed tincidunt rhoncus tortor, sed vestibulum nisi vulputate nec. Maecenas hendrerit, ipsum vitae egestas suscipit, lorem nibh dignissim lorem, ac placerat mi est eu risus. Vestibulum nisl nisl, auctor nec ante feugiat, maximus pretium mi.

    66 | 67 |

    Donec a malesuada nibh, in cursus eros. Pellentesque sit amet accumsan justo. Donec ut quam lobortis, sagittis erat eu, finibus leo. Proin vestibulum ex dui, id ultrices ligula vulputate quis. Suspendisse est sem, hendrerit sed augue quis, tempus iaculis metus. Etiam rutrum gravida interdum. Duis vitae auctor nunc. Praesent ac metus massa. In ligula sapien, aliquam consequat tincidunt a, iaculis at enim. Nulla facilisi. Cras vulputate ante purus, vitae volutpat ipsum dignissim sed. Mauris dapibus nisl tincidunt dolor blandit, sit amet ultricies neque bibendum. Sed tempor nunc id odio iaculis porta. Pellentesque ut scelerisque mauris, eu bibendum eros.

    68 | 69 |

    Morbi dignissim aliquet nunc eu dignissim. Nulla ultrices felis elit, a posuere massa tincidunt nec. Nulla facilisi. Maecenas lorem nisi, lobortis eu dolor vitae, pretium dapibus ante. Aenean lacus ex, fringilla venenatis tempus sit amet, mollis id tortor. Duis massa mauris, aliquam a lacus a, elementum finibus sem. Praesent elementum tortor est, ut condimentum justo ullamcorper nec. Quisque quis ligula eu ex venenatis sagittis in non dui.

    70 | 71 |

    Fusce mattis sagittis

    72 | 73 |

    Etiam condimentum, risus eu hendrerit mattis, ipsum nisi aliquam ipsum, a venenatis purus ipsum sit amet lorem. Maecenas tempus dolor nec elit pharetra semper. Nullam id nibh blandit, viverra sem non, porttitor est. Vestibulum ac sollicitudin ligula. Aliquam erat volutpat. Donec id neque a erat sagittis tempus. Duis feugiat velit eget ligula egestas suscipit. Cras pharetra auctor vestibulum. Nullam et lorem eu mi fermentum dignissim. Sed interdum enim ac sem faucibus, non faucibus est placerat. Sed pharetra lorem sit amet augue feugiat auctor.

    74 |
    75 |
    76 | 77 | -------------------------------------------------------------------------------- /examples/good-looking/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 43 | 44 | 45 | 46 |
    47 |
    48 |

    Punch

    49 | 50 |

    To make punch of any sort in perfection, the ambrosial essence of the lemon most be extracted by rubbing lumps of sugar on the rind, which breaks the delicate little vessels that contain the essence, and at the same time absorbs it. This, and making the mixture sweet and strong, using tea instead of water, and thoroughly amalgamating all the compounds, so that the taste of neither the bitter, the sweet, the spirit,- nor the element, shall be perceptible one over the other, is the grand secret, only to be acquired by practice.

    51 |

    In making hot toddy, or hot punch, you must put in the spirits before the water: in cold punch, grog, &c., the other way.

    52 |

    The precise portions of spirit and water, or even of the acidity and sweetness, can have no general rule, as scarcely two persons make punch alike.

    53 | 54 | 55 | 56 |

    Brandy Punch

    57 | 58 |

    Use large bar glass.

    59 | 60 |
      61 |
    • 1 table-spoonful raspberry syrup.
    • 62 |
    • 2 do. white sugar.
    • 63 |
    • 1 wine-glass water.
    • 64 |
    • 1 do. brandy.
    • 65 |
    • ½ small-sized lemon.
    • 66 |
    • 2 slices of orange.
    • 67 |
    • 1 piece of pine-apple.
    • 68 |
    69 | 70 |
    71 |

    Fill the tumbler with shaved ice, shake well, and dress the top with berries in season; sip through a straw.

    72 |

    73 | 74 |

    Brandy Punch

    75 | 76 |

    For a party of twenty.

    77 | 78 |
      79 |
    • 1 gallon of water.
    • 80 |
    • 3 quarts of brandy.
    • 81 |
    • ½ pint of Jamaica ram.
    • 82 |
    • 2 lbs. of sugar.
    • 83 |
    • Juice of 6 lemons.
    • 84 |
    • 3 oranges sliced.
    • 85 |
    • 1 pine-apple, pared, and cut up.
    • 86 |
    • 1 gill of Curaçao.
    • 87 |
    • 2 gills of raspberry syrup.
    • 88 |
    • Ice, and add berries in season.
    • 89 |
    90 | 91 |
    92 |

    Mix the materials well together in a large bowl, and you have a splendid punch.

    93 |
    94 | 95 |

    Mississippi Punch

    96 | 97 |

    Use large bar glass.

    98 | 99 |
  1. 100 |
  2. 1 wine-glass of brandy.
  3. 101 |
  4. ½ do. Jamaica rum.
  5. 102 |
  6. ½ do. Bourbon whiskey.
  7. 103 |
  8. ½ do. water.
  9. 104 |
  10. 1½ table-spoonful of powdered white sugar.
  11. 105 |
  12. ¼ of a large lemon.
  13. 106 | 107 | 108 |
    109 |

    Fill a tumbler with shaved ice.

    110 |

    The above must be well shaken, and to those who like their draughts "like linked sweetness long drawn out," let them use a glass tube or straw to sip the nectar through. The top of this punch should be ornamented with small pieces of orange, and berries in season.

    111 |
    112 |
    113 | 114 |
    115 |

    The Cobbler

    116 | 117 |

    Like the julep, this delicious potation is an American invention, although it is now a favorite in all warm climates. The "cobbler" does not require much skill in compounding, but to make it acceptable to the eye, as well as to the palate, it is necessary to display Home taste in ornamenting the glass after the beverage is made. We give an illustration showing how a cobbler should look when made to suit an epicure.

    118 | 119 |

    Sherry Cobbler

    120 | 121 |

    Use large bar glass.

    122 | 123 |
      124 |
    • 2 wine-glasses of sherry.
    • 125 |
    • 1 table-spoonful of sugar.
    • 126 |
    • 2 or 3 slices of orange.
    • 127 |
    128 | 129 |
    130 |

    Fill a tumbler with shaved ice, shake well and ornament with berries in season. Place a straw as represented in the wood-eat.

    131 |
    132 | 133 |

    Champagne Cobbler

    134 | 135 |

    One bottle of wine to four large bar glasses.

    136 | 137 |
      138 |
    • 1 table-spoonful of sugar.
    • 139 |
    • 1 piece each or orange and lemon peel.
    • 140 |
    141 | 142 |
    143 |

    Fill the tumbler one-third full with shaved ice, and fill balance with wine, ornament in a tasty manner with berries in season. This beverage should be sipped through a straw.

    144 |
    145 | 146 |

    Catawba Cobbler

    147 | 148 |

    Use large bar glass.

    149 | 150 |
      151 |
    • 1 teaspoonful of sugar dissolved in one table-spoonful of crater.
    • 152 |
    • 2 wineglasses of wine.
    • 153 |
    154 | 155 |
    156 |

    Fill tumbler with shaved ice, and ornament with sliced orange and berries in season. Place a straw as described in the sherry cobbler.

    157 |
    158 | 159 |

    Hock Cobbler

    160 | 161 |

    Use large bar glass.

    162 | 163 |
    164 |

    This drink is made the same way as the Catawba cobbler, using Hock wine instead of Catawba.

    165 |
    166 |
    167 | 168 |
    169 |

    Juleps

    170 | 171 | 172 | 173 |

    The julep is peculiarly an American beverage, and in the Southern states is more popular than any other. It was introduced into England by Captain Marryatt, where it is now quite a favorite. The gallant captain seems to have had a penchant for the nectarous drink, and publish- ed the recipe in his work on America. We give it in his own words: "I must descant a little upon the mint julep, as it is, with the thermometer at 100°, one of the most delightful and insinuating potations that ever was invented, and may be drunk with equal satisfaction when the thermometer is as low as 70°. There are many varieties, such as those composed of claret, Madeira, &c. ; but the ingredients of the real mint julep are as follows. I learned how to make them, and succeeded pretty well. Put into a tumbler about a dozen sprigs of the tender shoots of mint, upon them put a spoonful of white sugar, and equal pro- portions of peach and common brandy, so as to fill it up one-third, or perhaps a little less. Then take rasped or pounded ice, and fill up the tumbler. Epicures rub the lips of the tumbler with a piece of fresh pineapple, and the tumbler itself is very often encrusted outside with stalactites of ice. As the ice melts, you drink. I once over- heard two ladies talking in the next room to me, and one of them said, * Well, if I have a weakness for any one thing, it is for a mint julep!' — a very amiable weakness, and proving her good sense and good taste. _ They are, in fact, like the American ladies, irresistible."

    174 | 175 |

    Mint Julep

    176 | 177 |

    Use large bar glass.

    178 | 179 |
      180 |
    • 1 table-spoonful of white pulverized sugar.
    • 181 |
    • 2½ do. water, mix well with a spoon.
    • 182 |
    183 | 184 |
    185 |

    Take three or four sprigs of fresh mint, and press them well in the sugar arid water, until the flavor of the mint is extracted; add one and a half wine-glass of Cognac brandy, and fill the glass with fine shaved ice, then draw out the sprigs of mint and insert them in the ice with the stems downward, so that the leaves will be above, in the shape of a bouquet ; arrange berries, and small pieces of sliced orange on top in a tasty manner, dash with Jamaica rum, and sprinkle white sugar on top. Place a straw as represented in the cut, and you have a julep that is fit for an emperor.

    186 |
    187 | 188 |

    Brandy Julep

    189 | 190 |

    Use large bar glass.

    191 | 192 |
    193 |

    The brandy juice is made with the same ingredients as the mint julep, omitting the fancy fixings.

    194 |
    195 | 196 |

    Gin Julep

    197 | 198 |

    Use large bar glass.

    199 | 200 |
    201 |

    The gin julep is made with the same ingredients as the mint julep, omitting the fancy fixings.

    202 |
    203 | 204 |

    Whiskey Julep

    205 | 206 |

    Use large bar glass.

    207 | 208 |
    209 |

    The whiskey julep is made the same as the mint julep, omitting all fruits and berries.

    210 |
    211 | 212 |

    Pineapple Julep

    213 | 214 |

    For a party of five.

    215 | 216 |
    217 |

    Peel, slice, and cut up a ripe pineapple into a glass bowl, add the juice of two oranges, a gill of raspberry syrup, a gill of maraschino, a gill of old gin, a bottle of sparkling Moselle, and about a pound of pure ice in shaves; mix, ornament with berries in season, and serve in flat glasses.

    218 |
    219 |
    220 | 221 |
    225 |
    226 | 227 | 228 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Table of Contents (TOC) Generator 3 | 4 | [![Travis build status](http://img.shields.io/travis/gajus/contents/master.svg?style=flat-square)](https://travis-ci.org/gajus/contents) 5 | [![NPM version](http://img.shields.io/npm/v/contents.svg?style=flat-square)](https://www.npmjs.org/package/contents) 6 | 7 | 10 | 11 | Table of contents generator. 12 | 13 | * [Table of Contents (TOC) Generator](#table-of-contents-toc-generator) 14 | * [Usage](#table-of-contents-toc-generator-usage) 15 | * [Quick Start](#table-of-contents-toc-generator-usage-quick-start) 16 | * [Examples](#table-of-contents-toc-generator-usage-examples) 17 | * [Introduction of ES6 in 4.0.0](#table-of-contents-toc-generator-introduction-of-es6-in-4-0-0) 18 | * [Similar Libraries](#table-of-contents-toc-generator-similar-libraries) 19 | * [Required 3rd Party Libraries](#table-of-contents-toc-generator-similar-libraries-required-3rd-party-libraries) 20 | * [Smooth Scrolling](#table-of-contents-toc-generator-similar-libraries-smooth-scrolling) 21 | * [Window Resize and `scroll` Event Handling](#table-of-contents-toc-generator-similar-libraries-window-resize-and-scroll-event-handling) 22 | * [Table of Contents Array](#table-of-contents-array) 23 | * [Download](#table-of-contents-array-download) 24 | * [Configuration](#table-of-contents-array-configuration) 25 | * [Content Indexing](#table-of-contents-array-content-indexing) 26 | * [Hierarchy](#table-of-contents-array-content-indexing-hierarchy) 27 | * [Linking](#table-of-contents-array-linking) 28 | * [Article ID](#table-of-contents-array-linking-article-id) 29 | * [Markup](#table-of-contents-array-markup) 30 | * [Events](#table-of-contents-array-events) 31 | 32 | 33 | 34 | ## Usage 35 | 36 | 37 | ### Quick Start 38 | 39 | ```js 40 | import Contents from 'contents'; 41 | 42 | // This example generates a table of contents for all of the headings in the document. 43 | // Table of contents is an ordered list element. 44 | const contents = Contents(); 45 | 46 | // Append the generated list element (table of contents) to the container. 47 | document.querySelector('#your-table-of-contents-container').appendChild(contents.list()); 48 | 49 | // Attach event listeners: 50 | contents.eventEmitter().on('change', function () { 51 | console.log('User has navigated to a new section of the page.'); 52 | }); 53 | 54 | // The rest of the code illustrates firing "resize" event after you have 55 | // added new content after generating the table of contents. 56 | const newHeading = document.createElement('h2'); 57 | 58 | newHeading.innerHTML = 'Dynamically generated title'; 59 | 60 | document.body.appendChild(newHeading); 61 | 62 | // Firing the "resize" event will regenerate the table of contents. 63 | contents.eventEmitter().trigger('resize'); 64 | 65 | ``` 66 | 67 | 68 | ### Examples 69 | 70 | * [Good looking](http://gajus.com/sandbox/contents/examples/good-looking/) example. 71 | * [Plain](http://gajus.com/sandbox/contents/examples/plain/) table of contents not using jQuery. 72 | * [Events](http://gajus.com/sandbox/contents/examples/events/) table of contents with all events logged in the `console.log`. 73 | * [Obtain Generated List Element](http://gajus.com/sandbox/contents/examples/list-element/). 74 | * [jQuery](http://gajus.com/sandbox/contents/examples/jquery/) table of contents using jQuery. 75 | * [Smooth scrolling](http://gajus.com/sandbox/contents/examples/smooth-scrolling/) implemented using [jquery-smooth-scroll](https://github.com/kswedberg/jquery-smooth-scroll). 76 | 77 | The code for all of the examples is in the [examples](./examples/) folder. 78 | 79 | [Raise an issue](https://github.com/gajus/contents/issues) if you are missing an example. 80 | 81 | 82 | ## Introduction of ES6 in 4.0.0 83 | 84 | [Similar Libraries](#rimilar-libraries) stats have been generated in 22-Nov-14 08:44:41 UTC. Since then Contents has evolved a lot. The source code is written in ES6 and depends on `babel-core` to run. In projects that already depend on Babel and use webpack to build packages, this is not going to be a problem. Other projects need to consider the relatively heavy weight of the generated package. 85 | 86 | 87 | ## Similar Libraries 88 | 89 | | Feature | [contents](https://github.com/gajus/contents) | [toc](https://github.com/jgallen23/toc) | [jquery.tocify.js](https://github.com/gfranko/jquery.tocify.js) | 90 | | --- | --- | --- | --- | 91 | | Markup using nested `
      ` | ✓ | - | - | 92 | | [Smooth scrolling](#smooth-scrolling) | - | ✓ | ✓ | 93 | | Forward and back button support | ✓ | - | ✓ | 94 | | [Events](#events) | ✓ | - | - | 95 | | [Efficient `scroll` event](#window-resize-and-scroll-event-handling) | ✓ | ✓ | heading- | 96 | | [Reflect `window` resize](#window-resize-and-scroll-event-handling) | ✓ | - | ✓ | 97 | | [Extract table of contents as an array](#table-of-contents-array) | ✓ | - | - | 98 | | Overwrite markup and navigation | ✓ | - | - | 99 | | Can have multiple on a page | ✓ | ✓ | ✓ | 100 | | [Required 3rd party libraries](#required-3rd-party-libraries) | - | jQuery | jQuery, jQueryUI | 101 | | Size | < 6.000 kb | 2.581 kb | 7.246 kb | 102 | | GitHub Stars | 192 | 307 | 435 | 103 | 104 | Last updated: Saturday, 22-Nov-14 08:44:41 UTC. 105 | 106 | 107 | ### Required 3rd Party Libraries 108 | 109 | There are no 3rd party dependencies. jQuery selectors are used in the examples to make it simple for the reader. 110 | 111 | 112 | ### Smooth Scrolling 113 | 114 | You can implement smooth scrolling using either of the existing libraries. See [Integration Examples](#integration-examples). 115 | 116 | 117 | ### Window Resize and scroll Event Handling 118 | 119 | The library will index `offsetTop` of all articles. This index is used to reflect the [change event](#events). The index is built upon loading the page, and in response to `window.onresize` and [`ready`](#events) events. 120 | 121 | Reading `offsetTop` causes a [reflow](http://gent.ilcore.com/2011/03/how-not-to-trigger-layout-in-webkit.html). Therefore, this should not be done while scrolling. 122 | 123 | 124 | # Table of Contents Array 125 | 126 | You can extract the table of contents as a collection of nested objects representing the table of contents. 127 | 128 | ```js 129 | /** 130 | * @return {array} Array representation of the table of contents. 131 | */ 132 | contents.tree(); 133 | ``` 134 | 135 | Tree is a collection of nodes: 136 | 137 | ```js 138 | [ 139 | // Node 140 | { 141 | // Hierarchy level (e.g. h1 = 1) 142 | level: 1, 143 | // Id derived using articleId() function. 144 | id: '', 145 | // Name derived using articleName() function. 146 | name: '', 147 | // The article element. 148 | element: null, 149 | // Collection of the descendant nodes. 150 | descendants: [ /* node */ ] 151 | } 152 | ] 153 | ``` 154 | 155 | 156 | ## Download 157 | 158 | Using [NPM](https://www.npmjs.org/): 159 | 160 | ```sh 161 | npm install contents 162 | 163 | ``` 164 | 165 | 166 | ## Configuration 167 | 168 | | Name | Type | Description | 169 | | --- | --- | --- | 170 | | `articles` | `NodeList`, `jQuery` | (optional) The default behavior is to index all headings (H1-H6) in the document. See [Content Indexing](#content-indexing). | 171 | | `link` | `function` | (optional) Used to represent article in the table of contents and to setup navigation. See [Linking](#linking). | 172 | 173 | ## Content Indexing 174 | 175 | The default behavior is to index all headings (H1-H6) in the document. 176 | 177 | Use `articles` setting to index content using your own selector: 178 | 179 | ```js 180 | Contents({ 181 | articles: document.querySelectorAll('main h2, main h2') 182 | // If you are using jQuery 183 | // articles: $('main').find('h2, h3').get() 184 | }); 185 | 186 | ``` 187 | 188 | 189 | ### Hierarchy 190 | 191 | `articles` will be used to make the table of contents. `articles` have level of importance. The level of importance determines list nesting (see [Markup](#markup)). For HTML headings, the level of importance is derived from the tag name (``). To set your own level of importance, use `Contents.level` [dataset](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement.dataset) property or jQuery data property with the same name, e.g. 192 | 193 | ```js 194 | $('main').find('.summary').data('gajus.contents.level', 4); 195 | 196 | Contents({ 197 | articles: $('main').find('h1, h2, h3, .summary').get() 198 | }); 199 | 200 | ``` 201 | 202 | When level of importance cannot be determined, it defaults to 1. 203 | 204 | 205 | ## Linking 206 | 207 | `link` method is used to represent article in the table of contents and to setup navigation. This method is called once for each article after the list of the table of contents is generated. 208 | 209 | The default implementation: 210 | 211 | 1. Derives ID from the article 212 | 2. Generates a hyperlink using article ID as the anchor 213 | 3. Appends the URL to the table of contents 214 | 4. Wraps the article node in a self-referencing hyperlink. 215 | 216 | ```js 217 | /** 218 | * This function is called after the table of contents is generated. 219 | * It is called for each article in the index. 220 | * Used to represent article in the table of contents and to setup navigation. 221 | * 222 | * @param {HTMLElement} guide An element in the table of contents representing an article. 223 | * @param {HTMLElement} article The represented content element. 224 | */ 225 | Contents.link = (guide, article) => { 226 | const guideLink = document.createElement('a'), 227 | const articleLink = document.createElement('a'), 228 | const articleName = article.innerText, 229 | const articleId = article.id || Contents.id(articleName); 230 | 231 | article.id = articleId; 232 | 233 | articleLink.href = '#' + articleId; 234 | 235 | while (article.childNodes.length) { 236 | articleLink.appendChild(article.childNodes[0], articleLink); 237 | } 238 | 239 | article.appendChild(articleLink); 240 | 241 | guideLink.appendChild(document.createTextNode(articleName)); 242 | guideLink.href = '#' + articleId; 243 | guide.insertBefore(guideLink, guide.firstChild); 244 | }; 245 | 246 | ``` 247 | 248 | To overwrite the default behavior, you can provide your own `link` function as part of the configuration: 249 | 250 | ```js 251 | Contents({ 252 | // Example of implementation that does not wrap 253 | // article node in a hyperlink. 254 | link: (guide, article) => { 255 | var guideLink, 256 | articleName, 257 | articleId; 258 | 259 | guide = $(guide); 260 | article = $(article); 261 | 262 | guideLink = $(''); 263 | articleName = article.text(); 264 | articleId = article.attr('id') || Contents.id(articleName); 265 | 266 | guideLink 267 | .text(articleName) 268 | .attr('href', '#' + articleId) 269 | .prependTo(guide); 270 | 271 | article.attr('id', articleId); 272 | } 273 | }); 274 | 275 | ``` 276 | 277 | 278 | ### Article ID 279 | 280 | The default implementation relies on each article having an "id" attribute to enable anchor navigation. 281 | 282 | If you are overwriting the default `link` implementation, you can take advantage of the `Contents.id` function. 283 | 284 | `Contents.id` is responsible for deriving a unique ID from the text of the article, e.g. 285 | 286 | ```html 287 |

      Allow me to reiterate

      288 |

      Allow me to reiterate

      289 |

      Allow me to reiterate

      290 | 291 | ``` 292 | 293 | The default `link` implementation will use `Contents.id` to give each article a unique ID: 294 | 295 | ```html 296 |

      Allow me to reiterate

      297 |

      Allow me to reiterate

      298 |

      Allow me to reiterate

      299 | 300 | ``` 301 | 302 | 303 | ## Markup 304 | 305 | Table of contents is an ordered [list element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ol). List nesting reflects the heading hierarchy. The default behavior is to represent each heading using a hyperlink (See [Linking](#linking)), e.g. 306 | 307 | ```html 308 |

      JavaScript

      309 |

      History

      310 |

      Trademark

      311 |

      Features

      312 |

      Imperative and structured

      313 |

      Dynamic

      314 |

      Functional

      315 |

      Syntax

      316 | ``` 317 | 318 | Contents will generate the following markup for the above content: 319 | 320 | ```html 321 |
        322 |
      1. 323 | JavaScript 324 | 325 |
          326 |
        1. 327 | History 328 |
        2. 329 |
        3. 330 | Trademark 331 |
        4. 332 |
        5. 333 | Features 334 | 335 |
            336 |
          1. 337 | Imperative and structured 338 |
          2. 339 |
          3. 340 | Dynamic 341 |
          4. 342 |
          5. 343 | Functional 344 |
          6. 345 |
          346 |
        6. 347 |
        7. 348 | Syntax 349 |
        8. 350 |
        351 |
      2. 352 |
      353 | ``` 354 | 355 | 356 | ## Events 357 | 358 | | Event | Description | 359 | | --- | --- | 360 | | `ready` | Fired once after the table of contents has been generated. | 361 | | `resize` | Fired when the page is loaded and in response to "resize" and "orientationchange" `window` events. | 362 | | `change` | Fired when the page is loaded and when user navigates to a new section of the page. | 363 | 364 | Attach event listeners using the `eventEmitter.on` of the resulting Contents object: 365 | 366 | ```js 367 | const contents = Contents(); 368 | 369 | contents.eventEmitter.on('ready', () => {}); 370 | contents.eventEmitter.on('resize', () => {}); 371 | 372 | ``` 373 | 374 | The `change` event listener is passed extra parameters: `.current.article`, `.current.guide`, and when available, `.previous.article`, `.previous.guide`: 375 | 376 | ```js 377 | contents.eventEmitter.on('change', (data) => { 378 | if (data.previous) { 379 | $(data.previous.article).removeClass('active-article'); 380 | $(data.previous.guide).removeClass('active-guide'); 381 | } 382 | 383 | $(data.current.article).addClass('active-article'); 384 | $(data.current.guide).addClass('active-guide'); 385 | }); 386 | 387 | ``` 388 | 389 | You must trigger "resize" event after programmatically changing the content or the presentation of the content.: 390 | 391 | ```js 392 | contents.eventEmitter.trigger('resize'); 393 | 394 | ``` 395 | 396 | This is required to recalculate the position of the content. 397 | 398 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import Sister from 'sister'; 3 | 4 | /** 5 | * @param {Object} config 6 | * @return {Contents} 7 | */ 8 | const Contents = (config) => { 9 | const contents = {}; 10 | 11 | const eventEmitter = Sister(); 12 | 13 | const instanceConfig = Contents.config(config); 14 | 15 | const articles = Contents.articles(instanceConfig.articles, instanceConfig.articleName, instanceConfig.articleId); 16 | const tree = Contents.tree(articles); 17 | const list = Contents.list(tree, instanceConfig.link); 18 | 19 | Contents.bind(eventEmitter, list, instanceConfig); 20 | 21 | /** 22 | * @return {HTMLElement} Ordered list element representation of the table of contents. 23 | */ 24 | contents.list = () => { 25 | return list; 26 | }; 27 | 28 | /** 29 | * @return {Array} Array representation of the table of contents. 30 | */ 31 | contents.tree = () => { 32 | return tree; 33 | }; 34 | 35 | /** 36 | * @return {Sister} Event emitter used to attach event listeners and trigger events. 37 | */ 38 | contents.eventEmitter = () => { 39 | return eventEmitter; 40 | }; 41 | 42 | return contents; 43 | }; 44 | 45 | /** 46 | * Setups event listeners to reflect changes to the table of contents and user navigation. 47 | * 48 | * @param {Sister} eventEmitter 49 | * @param {HTMLElement} list Table of contents root element (
        ). 50 | * @param {Object} config Result of contents.config. 51 | * @return {Object} Result of contents.eventEmitter. 52 | */ 53 | Contents.bind = (eventEmitter, list, config) => { 54 | let articleOffsetIndex; 55 | let lastArticleIndex = null; 56 | let windowHeight; 57 | 58 | const guides = list.querySelectorAll('li'); 59 | 60 | eventEmitter.on('resize', () => { 61 | windowHeight = Contents.windowHeight(); 62 | articleOffsetIndex = Contents.indexOffset(config.articles); 63 | 64 | eventEmitter.trigger('scroll'); 65 | }); 66 | 67 | eventEmitter.on('scroll', () => { 68 | let changeEvent; 69 | 70 | const articleIndex = Contents.getIndexOfClosestValue(Contents.windowScrollY() + windowHeight * 0.2, articleOffsetIndex); 71 | 72 | if (articleIndex !== lastArticleIndex) { 73 | changeEvent = {}; 74 | 75 | changeEvent.current = { 76 | article: config.articles[articleIndex], 77 | guide: guides[articleIndex] 78 | }; 79 | 80 | if (lastArticleIndex !== null) { 81 | changeEvent.previous = { 82 | article: config.articles[lastArticleIndex], 83 | guide: guides[lastArticleIndex] 84 | }; 85 | } 86 | 87 | eventEmitter.trigger('change', changeEvent); 88 | 89 | lastArticleIndex = articleIndex; 90 | } 91 | }); 92 | 93 | // This allows the script that constructs Contents 94 | // to catch the first ready, resize and scroll events. 95 | setTimeout(() => { 96 | eventEmitter.trigger('resize'); 97 | eventEmitter.trigger('ready'); 98 | 99 | global.window.addEventListener('resize', Contents.throttle(() => { 100 | eventEmitter.trigger('resize'); 101 | }, 100)); 102 | 103 | global.window.addEventListener('scroll', Contents.throttle(() => { 104 | eventEmitter.trigger('scroll'); 105 | }, 100)); 106 | }, 10); 107 | }; 108 | 109 | /** 110 | * @return {number} 111 | */ 112 | Contents.windowHeight = () => { 113 | return global.innerHeight || global.document.documentElement.clientHeight; 114 | }; 115 | 116 | /** 117 | * @return {number} 118 | */ 119 | Contents.windowScrollY = () => { 120 | return global.pageYOffset || global.document.documentElement.scrollTop; 121 | }; 122 | 123 | /** 124 | * Interpret execution configuration. 125 | * 126 | * @param {Object} userConfig 127 | * @return {Object} 128 | */ 129 | Contents.config = (userConfig = {}) => { 130 | const properties = [ 131 | 'articles', 132 | 'articleName', 133 | 'articleId', 134 | 'link' 135 | ]; 136 | 137 | const difference = _.difference(Object.keys(userConfig), properties); 138 | 139 | if (difference.length) { 140 | throw new Error('Unknown configuration property "' + difference[0] + '".'); 141 | } 142 | 143 | const defaultConfig = { 144 | articleId: Contents.articleId, 145 | articleName: Contents.articleName, 146 | articles: global.document.querySelectorAll('h1, h2, h3, h4, h5, h6'), 147 | link: Contents.link 148 | }; 149 | 150 | const instanceConfig = _.assign({}, defaultConfig, userConfig); 151 | 152 | if (!instanceConfig.articles.length || !(instanceConfig.articles[0] instanceof global.window.HTMLElement)) { 153 | throw new Error('Option "articles" is not a collection of HTMLElement objects.'); 154 | } 155 | 156 | if (typeof instanceConfig.articleName !== 'function') { 157 | throw new TypeError('Option "articleName" must be a function.'); 158 | } 159 | 160 | if (typeof instanceConfig.articleId !== 'function') { 161 | throw new TypeError('Option "articleId" must be a function.'); 162 | } 163 | 164 | if (typeof instanceConfig.link !== 'function') { 165 | throw new TypeError('Option "link" must be a function.'); 166 | } 167 | 168 | return instanceConfig; 169 | }; 170 | 171 | /** 172 | * Derive article name. 173 | * 174 | * This method can be overwritten using config.articleName. 175 | * 176 | * @param {HTMLElement} element 177 | * @return {string} 178 | */ 179 | Contents.articleName = (element) => { 180 | return element.innerText || element.textContent; 181 | }; 182 | 183 | /** 184 | * Derive article ID. 185 | * 186 | * This method can be overwritten using config.articleId. 187 | * 188 | * @param {string} articleName 189 | * @param {HTMLElement} element 190 | * @return {string} 191 | */ 192 | Contents.articleId = (articleName, element) => { 193 | return element.id || articleName; 194 | }; 195 | 196 | /** 197 | * Make element ID unique in the context of the document. 198 | * 199 | * @param {string} inputId 200 | * @param {Array} existingIDs Existing IDs in the document. Required for markup-contents. (https://github.com/gajus/markdown-contents) 201 | * @return {string} 202 | */ 203 | Contents.uniqueID = (inputId, existingIDs) => { 204 | let assignedId; 205 | 206 | let i = 1; 207 | 208 | const formattedId = Contents.formatId(inputId); 209 | 210 | if (existingIDs) { 211 | assignedId = formattedId; 212 | 213 | while (existingIDs.indexOf(assignedId) !== -1) { 214 | assignedId = formattedId + '-' + i++; 215 | } 216 | 217 | existingIDs.push(assignedId); 218 | } else { 219 | if (!global.document) { 220 | throw new Error('No document context.'); 221 | } 222 | 223 | assignedId = formattedId; 224 | 225 | while (global.document.querySelector('#' + assignedId)) { 226 | assignedId = formattedId + '-' + i++; 227 | } 228 | } 229 | 230 | return assignedId; 231 | }; 232 | 233 | /** 234 | * Formats text into an ID/anchor safe value. 235 | * 236 | * @see http://stackoverflow.com/a/1077111/368691 237 | * @param {string} str 238 | * @return {string} 239 | */ 240 | Contents.formatId = (str) => { 241 | return str 242 | .toLowerCase() 243 | .replace(/[ãàáäâ]/g, 'a') 244 | .replace(/[ẽèéëê]/g, 'e') 245 | .replace(/[ìíïî]/g, 'i') 246 | .replace(/[õòóöô]/g, 'o') 247 | .replace(/[ùúüû]/g, 'u') 248 | .replace(/[ñ]/g, 'n') 249 | .replace(/[ç]/g, 'c') 250 | .replace(/\s+/g, '-') 251 | .replace(/[^a-z0-9\-_]+/g, '-') 252 | .replace(/-+/g, '-') 253 | .replace(/^-|-$/g, '') 254 | .replace(/^[^a-z]+/g, ''); 255 | }; 256 | 257 | /** 258 | * Generate flat index of the articles. 259 | * 260 | * @param {Array} elements 261 | * @param {Contents.articleName} articleName 262 | * @param {Contents.articleId} articleId 263 | * @return {Array} 264 | */ 265 | Contents.articles = (elements, articleName = Contents.articleName, articleId = Contents.articleId) => { 266 | return _.map(elements, (element) => { 267 | const article = {}; 268 | 269 | article.level = Contents.level(element); 270 | article.name = articleName(element); 271 | article.id = articleId(article.name, element); 272 | article.element = element; 273 | 274 | return article; 275 | }); 276 | }; 277 | 278 | /** 279 | * Makes hierarchical index of the articles from a flat index. 280 | * 281 | * @param {Array} articles Generated using Contents.articles. 282 | * @param {boolean} makeUniqueIDs 283 | * @param {Array} uniqueIDpool 284 | * @return {Array} 285 | */ 286 | Contents.tree = (articles, makeUniqueIDs = true, uniqueIDpool = []) => { 287 | let lastNode; 288 | 289 | const rootNode = { 290 | descendants: [], 291 | level: 0 292 | }; 293 | 294 | const tree = rootNode.descendants; 295 | 296 | _.forEach(articles, (article) => { 297 | if (makeUniqueIDs) { 298 | article.id = Contents.uniqueID(article.id, uniqueIDpool); 299 | } 300 | article.descendants = []; 301 | 302 | if (!lastNode) { 303 | tree.push(article); 304 | } else if (lastNode.level === article.level) { 305 | Contents.tree.findParentNode(lastNode, rootNode).descendants.push(article); 306 | } else if (article.level > lastNode.level) { 307 | lastNode.descendants.push(article); 308 | } else { 309 | Contents.tree.findParentNodeWithLevelLower(lastNode, article.level, rootNode).descendants.push(article); 310 | } 311 | 312 | lastNode = article; 313 | }); 314 | 315 | return tree; 316 | }; 317 | 318 | /** 319 | * Find the object whose descendant is the needle object. 320 | * 321 | * @param {Object} needle 322 | * @param {Object} haystack 323 | * @return {HTMLElement} 324 | */ 325 | Contents.tree.findParentNode = (needle, haystack) => { 326 | if (haystack.descendants.indexOf(needle) !== -1) { 327 | return haystack; 328 | } 329 | 330 | let parent; 331 | let i = haystack.descendants.length; 332 | 333 | while (i--) { 334 | parent = Contents.tree.findParentNode(needle, haystack.descendants[i]); 335 | 336 | if (parent) { 337 | return parent; 338 | } 339 | } 340 | 341 | throw new Error('Invalid tree.'); 342 | }; 343 | 344 | /** 345 | * Find the object whose descendant is the needle object. 346 | * Look for parent (including parents of the found object) with level lower than level. 347 | * 348 | * @param {Object} needle 349 | * @param {number} level 350 | * @param {Object} haystack 351 | * @return {HTMLElement} 352 | */ 353 | Contents.tree.findParentNodeWithLevelLower = (needle, level, haystack) => { 354 | const parent = Contents.tree.findParentNode(needle, haystack); 355 | 356 | if (parent.level < level) { 357 | return parent; 358 | } else { 359 | return Contents.tree.findParentNodeWithLevelLower(parent, level, haystack); 360 | } 361 | }; 362 | 363 | /** 364 | * Generate ordered list from a tree (see tree) object. 365 | * 366 | * @param {Array} tree 367 | * @param {Function} link Used to customize the destination element in the list and the source element. 368 | * @return {HTMLElement} 369 | */ 370 | Contents.list = (tree, link) => { 371 | const list = global.document.createElement('ol'); 372 | 373 | _.forEach(tree, (article) => { 374 | const li = global.document.createElement('li'); 375 | 376 | if (link) { 377 | link(li, article); 378 | } 379 | 380 | if (article.descendants.length) { 381 | li.appendChild(Contents.list(article.descendants, link)); 382 | } 383 | 384 | list.appendChild(li); 385 | }); 386 | 387 | return list; 388 | }; 389 | 390 | /** 391 | * This function is called after the table of contents is generated. 392 | * It is called for each article in the index. 393 | * Used to represent article in the table of contents and to setup navigation. 394 | * 395 | * @todo wrong description 396 | * @param {HTMLElement} guide An element in the table of contents representing an article. 397 | * @param {Object} article {level, id, name, element, descendants} 398 | * @return {undefined} 399 | */ 400 | Contents.link = (guide, article) => { 401 | const guideLink = global.document.createElement('a'); 402 | const articleLink = global.document.createElement('a'); 403 | 404 | article.element.id = article.id; 405 | 406 | articleLink.href = '#' + article.id; 407 | 408 | while (article.element.childNodes.length) { 409 | articleLink.appendChild(article.element.childNodes[0]); 410 | } 411 | 412 | article.element.appendChild(articleLink); 413 | 414 | guideLink.appendChild(global.document.createTextNode(article.name)); 415 | guideLink.href = '#' + article.id; 416 | 417 | guide.insertBefore(guideLink, guide.firstChild); 418 | }; 419 | 420 | /** 421 | * Extract element level used to construct list hierarchy, e.g.

        is 1,

        is 2. 422 | * When element is not a heading, use Contents.level data attribute. Default to 1. 423 | * 424 | * @param {HTMLElement} element 425 | * @return {number} 426 | */ 427 | Contents.level = (element) => { 428 | const tagName = element.tagName.toLowerCase(); 429 | 430 | if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].indexOf(tagName) !== -1) { 431 | return parseInt(tagName.slice(1), 10); 432 | } 433 | 434 | if (typeof element.dataset['gajus.contents.level'] !== 'undefined') { 435 | return parseInt(element.dataset['gajus.contents.level'], 10); 436 | } 437 | 438 | return 1; 439 | }; 440 | 441 | /** 442 | * Produce a list of offset values for each element. 443 | * 444 | * @param {NodeList} elements 445 | * @return {Array} 446 | */ 447 | Contents.indexOffset = (elements) => { 448 | let element; 449 | let offset; 450 | const scrollYIndex = []; 451 | let i = 0; 452 | const j = elements.length; 453 | 454 | while (i < j) { 455 | element = elements[i++]; 456 | 457 | offset = element.offsetTop; 458 | 459 | // element.offsetTop might produce a float value. 460 | // Round to the nearest multiple of 5 (either up or down). 461 | // This is done to help readability and testing. 462 | offset = 5 * Math.round(offset / 5); 463 | 464 | scrollYIndex.push(offset); 465 | } 466 | 467 | return scrollYIndex; 468 | }; 469 | 470 | /** 471 | * Find the nearest value to the needle in the haystack and return the value index. 472 | * 473 | * @see http://stackoverflow.com/a/26366951/368691 474 | * @param {number} needle 475 | * @param {Array} haystack 476 | * @return {number} 477 | */ 478 | Contents.getIndexOfClosestValue = (needle, haystack) => { 479 | let lastClosestValueIndex; 480 | 481 | let closestValueIndex = 0; 482 | let i = 0; 483 | 484 | const j = haystack.length; 485 | 486 | if (!j) { 487 | throw new Error('Haystack must be not empty.'); 488 | } 489 | 490 | while (i < j) { 491 | if (Math.abs(needle - haystack[closestValueIndex]) > Math.abs(haystack[i] - needle)) { 492 | closestValueIndex = i; 493 | } 494 | 495 | if (closestValueIndex === lastClosestValueIndex) { 496 | break; 497 | } 498 | 499 | lastClosestValueIndex = closestValueIndex; 500 | 501 | i++; 502 | } 503 | 504 | return closestValueIndex; 505 | }; 506 | 507 | /** 508 | * @callback throttleCallback 509 | * @param {...*} var_args 510 | */ 511 | 512 | /** 513 | * Creates and returns a new, throttled version of the passed function, that, when invoked repeatedly, 514 | * will only call the original function at most once per every wait milliseconds. 515 | * 516 | * @see https://remysharp.com/2010/07/21/throttling-function-calls 517 | * @param {throttleCallback} throttled 518 | * @param {number} threshold Number of milliseconds between firing the throttled function. 519 | * @param {Object} context The value of "this" provided for the call to throttled. 520 | * @returns {Function} 521 | */ 522 | Contents.throttle = (throttled, threshold = 250, context = {}) => { 523 | let deferTimer; 524 | let last; 525 | 526 | return (...args) => { 527 | const now = Number(new Date()); 528 | 529 | if (last && now < last + threshold) { 530 | clearTimeout(deferTimer); 531 | deferTimer = setTimeout(() => { 532 | last = now; 533 | Reflect.apply(throttled, context, args); 534 | }, threshold); 535 | } else { 536 | last = now; 537 | Reflect.apply(throttled, context, args); 538 | } 539 | }; 540 | }; 541 | 542 | export default Contents; 543 | -------------------------------------------------------------------------------- /test/contents.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { 4 | JSDOM 5 | } from 'jsdom'; 6 | import { 7 | expect 8 | } from 'chai'; 9 | import Contents from '../src'; 10 | 11 | const createElement = (html) => { 12 | return (new JSDOM(html)).window.document.firstChild; 13 | }; 14 | 15 | describe('contents', () => { 16 | beforeEach(() => { 17 | const page = fs.readFileSync(path.resolve(__dirname, 'fixtures/page.html'), 'utf8'); 18 | 19 | const dom = new JSDOM(page); 20 | 21 | global.window = dom.window; 22 | global.document = dom.window.document; 23 | }); 24 | 25 | describe('DOM dependent method', () => { 26 | describe('.uniqueID()', () => { 27 | describe('when in the context of the document', () => { 28 | it('does not affect a unique ID', () => { 29 | expect(Contents.uniqueID('id-unique')).to.equal('id-unique'); 30 | }); 31 | it('derives a unique ID', () => { 32 | expect(Contents.uniqueID('id-not-unique')).to.equal('id-not-unique-1'); 33 | }); 34 | }); 35 | describe('when in the context of an array', () => { 36 | it('does not affect a unique ID', () => { 37 | expect(Contents.uniqueID('id-unique', [])).to.equal('id-unique'); 38 | }); 39 | it('derives a unique ID', () => { 40 | expect(Contents.uniqueID('id-not-unique', ['id-not-unique'])).to.equal('id-not-unique-1'); 41 | }); 42 | it('updates the haystack', () => { 43 | const haystack = ['id-not-unique']; 44 | 45 | Contents.uniqueID('id-not-unique', haystack); 46 | 47 | expect(haystack).to.deep.equal(['id-not-unique', 'id-not-unique-1']); 48 | }); 49 | }); 50 | }); 51 | describe('.level()', () => { 52 | it('derives level for heading element', () => { 53 | expect(Contents.level(createElement('

        '))).to.equal(1); 54 | }); 55 | describe('when element is not heading', () => { 56 | let element; 57 | 58 | beforeEach(() => { 59 | element = createElement(''); 60 | }); 61 | it('defaults to 1', () => { 62 | expect(Contents.level(element)).to.equal(1); 63 | }); 64 | it('uses gajus.contents.level dataset property', () => { 65 | element.dataset['gajus.contents.level'] = 2; 66 | 67 | expect(Contents.level(element)).to.equal(2); 68 | }); 69 | }); 70 | }); 71 | describe('.link()', () => { 72 | let article; 73 | let guide; 74 | 75 | beforeEach(() => { 76 | guide = document.createElement('div'); 77 | article = { 78 | element: document.createElement('div'), 79 | id: 'foo', 80 | name: 'Foo' 81 | }; 82 | Contents.link(guide, article); 83 | }); 84 | it('gives article an id', () => { 85 | expect(article.id).to.equal('foo'); 86 | }); 87 | it('wraps article content in a hyperlink', () => { 88 | expect(article.element.innerHTML).to.equal(''); 89 | }); 90 | it('appends a hyperlink to the guide', () => { 91 | expect(guide.innerHTML).to.equal('Foo'); 92 | }); 93 | }); 94 | describe.skip('.indexOffset()', () => { 95 | let offsetIndex; 96 | 97 | beforeEach(() => { 98 | offsetIndex = Contents.indexOffset(document.querySelectorAll('#index_offset div')); 99 | }); 100 | it('returns vertical offset of all elements', () => { 101 | expect(offsetIndex).to.deep.equal([0, 100, 200]); 102 | }); 103 | }); 104 | describe('.articles()', () => { 105 | const removeElementProperty = (tree) => { 106 | tree.forEach((node) => { 107 | Reflect.deleteProperty(node, 'element'); 108 | }); 109 | }; 110 | 111 | it('represents a flat structure', () => { 112 | const articles = Contents.articles(document.querySelectorAll('#make_articles h1')); 113 | 114 | const expectedArticles = [ 115 | { 116 | id: 'A1', 117 | level: 1, 118 | name: 'A1' 119 | }, 120 | { 121 | id: 'B1', 122 | level: 1, 123 | name: 'B1' 124 | }, 125 | { 126 | id: 'C1', 127 | level: 1, 128 | name: 'C1' 129 | } 130 | ]; 131 | 132 | removeElementProperty(articles); 133 | 134 | expect(articles).to.deep.equal(expectedArticles); 135 | }); 136 | }); 137 | }); 138 | describe('DOM independent method', () => { 139 | describe('.tree()', () => { 140 | const removeElementProperty = (tree) => { 141 | tree.forEach((node) => { 142 | Reflect.deleteProperty(node, 'element'); 143 | 144 | removeElementProperty(node.descendants); 145 | }); 146 | }; 147 | 148 | it('represents a flat structure', () => { 149 | const articles = [ 150 | { 151 | id: 'a1', 152 | level: 1, 153 | name: 'A1' 154 | }, 155 | { 156 | id: 'b1', 157 | level: 1, 158 | name: 'B1' 159 | }, 160 | { 161 | id: 'c1', 162 | level: 1, 163 | name: 'C1' 164 | } 165 | ]; 166 | 167 | const tree = Contents.tree(articles); 168 | 169 | const expectedTree = [ 170 | { 171 | descendants: [], 172 | id: 'a1', 173 | level: 1, 174 | name: 'A1' 175 | }, 176 | { 177 | descendants: [], 178 | id: 'b1', 179 | level: 1, 180 | name: 'B1' 181 | }, 182 | { 183 | descendants: [], 184 | id: 'c1', 185 | level: 1, 186 | name: 'C1' 187 | } 188 | ]; 189 | 190 | removeElementProperty(tree); 191 | 192 | expect(tree).to.deep.equal(expectedTree); 193 | }); 194 | it('represents an ascending hierarchy', () => { 195 | const articles = [ 196 | { 197 | id: 'a1', 198 | level: 1, 199 | name: 'A1' 200 | }, 201 | { 202 | id: 'b1', 203 | level: 2, 204 | name: 'B1' 205 | }, 206 | { 207 | id: 'c1', 208 | level: 3, 209 | name: 'C1' 210 | } 211 | ]; 212 | 213 | const tree = Contents.tree(articles); 214 | 215 | const a1 = { 216 | descendants: [], 217 | id: 'a1', 218 | level: 1, 219 | name: 'A1' 220 | }; 221 | const b1 = { 222 | descendants: [], 223 | id: 'b1', 224 | level: 2, 225 | name: 'B1' 226 | }; 227 | const c1 = { 228 | descendants: [], 229 | id: 'c1', 230 | level: 3, 231 | name: 'C1' 232 | }; 233 | 234 | a1.descendants = [b1]; 235 | b1.descendants = [c1]; 236 | 237 | const expectedTree = [a1]; 238 | 239 | removeElementProperty(tree); 240 | 241 | expect(tree).to.deep.equal(expectedTree); 242 | }); 243 | it('represents a multiple children', () => { 244 | const articles = [ 245 | { 246 | id: 'a1', 247 | level: 1, 248 | name: 'A1' 249 | }, 250 | { 251 | id: 'b1', 252 | level: 2, 253 | name: 'B1' 254 | }, 255 | { 256 | id: 'b2', 257 | level: 2, 258 | name: 'B2' 259 | } 260 | ]; 261 | 262 | const tree = Contents.tree(articles); 263 | 264 | const a1 = { 265 | descendants: [], 266 | id: 'a1', 267 | level: 1, 268 | name: 'A1' 269 | }; 270 | const b1 = { 271 | descendants: [], 272 | id: 'b1', 273 | level: 2, 274 | name: 'B1' 275 | }; 276 | const b2 = { 277 | descendants: [], 278 | id: 'b2', 279 | level: 2, 280 | name: 'B2' 281 | }; 282 | 283 | a1.descendants = [b1, b2]; 284 | 285 | const expectedTree = [a1]; 286 | 287 | removeElementProperty(tree); 288 | 289 | expect(tree).to.deep.equal(expectedTree); 290 | }); 291 | it('represents a descending hierarchy', () => { 292 | const articles = [ 293 | { 294 | id: 'a1', 295 | level: 1, 296 | name: 'A1' 297 | }, 298 | { 299 | id: 'b1', 300 | level: 2, 301 | name: 'B1' 302 | }, 303 | { 304 | id: 'c1', 305 | level: 3, 306 | name: 'C1' 307 | }, 308 | { 309 | id: 'b2', 310 | level: 2, 311 | name: 'B2' 312 | }, 313 | { 314 | id: 'a2', 315 | level: 1, 316 | name: 'A2' 317 | } 318 | ]; 319 | 320 | const tree = Contents.tree(articles); 321 | 322 | const a1 = { 323 | descendants: [], 324 | id: 'a1', 325 | level: 1, 326 | name: 'A1' 327 | }; 328 | const b1 = { 329 | descendants: [], 330 | id: 'b1', 331 | level: 2, 332 | name: 'B1' 333 | }; 334 | const c1 = { 335 | descendants: [], 336 | id: 'c1', 337 | level: 3, 338 | name: 'C1' 339 | }; 340 | const b2 = { 341 | descendants: [], 342 | id: 'b2', 343 | level: 2, 344 | name: 'B2' 345 | }; 346 | const a2 = { 347 | descendants: [], 348 | id: 'a2', 349 | level: 1, 350 | name: 'A2' 351 | }; 352 | 353 | a1.descendants = [b1, b2]; 354 | b1.descendants = [c1]; 355 | 356 | const expectedTree = [a1, a2]; 357 | 358 | removeElementProperty(tree); 359 | 360 | expect(tree).to.deep.equal(expectedTree); 361 | }); 362 | it('represents a descending hierarchy with gaps', () => { 363 | const articles = [ 364 | { 365 | id: 'a1', 366 | level: 1, 367 | name: 'A1' 368 | }, 369 | { 370 | id: 'b1', 371 | level: 2, 372 | name: 'B1' 373 | }, 374 | { 375 | id: 'c1', 376 | level: 3, 377 | name: 'C1' 378 | }, 379 | { 380 | id: 'a2', 381 | level: 1, 382 | name: 'A2' 383 | } 384 | ]; 385 | 386 | const tree = Contents.tree(articles); 387 | 388 | const a1 = { 389 | descendants: [], 390 | id: 'a1', 391 | level: 1, 392 | name: 'A1' 393 | }; 394 | const b1 = { 395 | descendants: [], 396 | id: 'b1', 397 | level: 2, 398 | name: 'B1' 399 | }; 400 | const c1 = { 401 | descendants: [], 402 | id: 'c1', 403 | level: 3, 404 | name: 'C1' 405 | }; 406 | const a2 = { 407 | descendants: [], 408 | id: 'a2', 409 | level: 1, 410 | name: 'A2' 411 | }; 412 | 413 | a1.descendants = [b1]; 414 | b1.descendants = [c1]; 415 | 416 | const expectedTree = [a1, a2]; 417 | 418 | removeElementProperty(tree); 419 | 420 | expect(tree).to.deep.equal(expectedTree); 421 | }); 422 | }); 423 | describe('.list()', () => { 424 | it('represents a flat structure', () => { 425 | const articles = [ 426 | { 427 | id: 'a1', 428 | level: 1, 429 | name: 'A1' 430 | }, 431 | { 432 | id: 'b1', 433 | level: 1, 434 | name: 'B1' 435 | }, 436 | { 437 | id: 'c1', 438 | level: 1, 439 | name: 'C1' 440 | } 441 | ]; 442 | 443 | const tree = Contents.tree(articles); 444 | const list = Contents.list(tree, (listElement, article) => { 445 | listElement.innerHTML = article.name; 446 | }); 447 | 448 | expect(list.outerHTML).to.equal('
        1. A1
        2. B1
        3. C1
        '); 449 | }); 450 | it('represents an ascending hierarchy', () => { 451 | const articles = [ 452 | { 453 | id: 'a1', 454 | level: 1, 455 | name: 'A1' 456 | }, 457 | { 458 | id: 'b1', 459 | level: 2, 460 | name: 'B1' 461 | }, 462 | { 463 | id: 'c1', 464 | level: 3, 465 | name: 'C1' 466 | } 467 | ]; 468 | 469 | const tree = Contents.tree(articles); 470 | const list = Contents.list(tree, (listElement, article) => { 471 | listElement.innerHTML = article.name; 472 | }); 473 | 474 | expect(list.outerHTML).to.equal('
        1. A1
          1. B1
            1. C1
        '); 475 | }); 476 | it('represents a multiple children', () => { 477 | const articles = [ 478 | { 479 | id: 'a1', 480 | level: 1, 481 | name: 'A1' 482 | }, 483 | { 484 | id: 'b1', 485 | level: 2, 486 | name: 'B1' 487 | }, 488 | { 489 | id: 'b2', 490 | level: 2, 491 | name: 'B2' 492 | } 493 | ]; 494 | 495 | const tree = Contents.tree(articles); 496 | const list = Contents.list(tree, (listElement, article) => { 497 | listElement.innerHTML = article.name; 498 | }); 499 | 500 | expect(list.outerHTML).to.equal('
        1. A1
          1. B1
          2. B2
        '); 501 | }); 502 | it('represents a descending hierarchy', () => { 503 | const articles = [ 504 | { 505 | id: 'a1', 506 | level: 1, 507 | name: 'A1' 508 | }, 509 | { 510 | id: 'b1', 511 | level: 2, 512 | name: 'B1' 513 | }, 514 | { 515 | id: 'c1', 516 | level: 3, 517 | name: 'C1' 518 | }, 519 | { 520 | id: 'b2', 521 | level: 2, 522 | name: 'B2' 523 | }, 524 | { 525 | id: 'a2', 526 | level: 1, 527 | name: 'A2' 528 | } 529 | ]; 530 | 531 | const tree = Contents.tree(articles); 532 | 533 | const list = Contents.list(tree, (listElement, article) => { 534 | listElement.innerHTML = article.name; 535 | }); 536 | 537 | expect(list.outerHTML).to.equal('
        1. A1
          1. B1
            1. C1
          2. B2
        2. A2
        '); 538 | }); 539 | it('represents a descending hierarchy with gaps', () => { 540 | const articles = [ 541 | { 542 | id: 'a1', 543 | level: 1, 544 | name: 'A1' 545 | }, 546 | { 547 | id: 'b1', 548 | level: 2, 549 | name: 'B1' 550 | }, 551 | { 552 | id: 'c1', 553 | level: 3, 554 | name: 'C1' 555 | }, 556 | { 557 | id: 'a2', 558 | level: 1, 559 | name: 'A2' 560 | } 561 | ]; 562 | 563 | const tree = Contents.tree(articles); 564 | 565 | const list = Contents.list(tree, (listElement, article) => { 566 | listElement.innerHTML = article.name; 567 | }); 568 | 569 | expect(list.outerHTML).to.equal('
        1. A1
          1. B1
            1. C1
        2. A2
        '); 570 | }); 571 | }); 572 | describe('.getIndexOfClosestValue()', () => { 573 | it('throws an error when the haystack is empty', () => { 574 | expect(() => { 575 | Contents.getIndexOfClosestValue(1, []); 576 | }).to.throw(Error, 'Haystack must be not empty.'); 577 | }); 578 | it('returns index of the first value when haystack length is 1', () => { 579 | expect(Contents.getIndexOfClosestValue(1, [100])).to.equal(0); 580 | }); 581 | it('rounds down to the nearest value', () => { 582 | expect(Contents.getIndexOfClosestValue(12, [1, 10, 20])).to.equal(1); 583 | }); 584 | it('returns index of the exact value', () => { 585 | expect(Contents.getIndexOfClosestValue(10, [1, 10, 20])).to.equal(1); 586 | }); 587 | it('rounds up to the nearest value', () => { 588 | expect(Contents.getIndexOfClosestValue(8, [1, 10, 20])).to.equal(1); 589 | }); 590 | }); 591 | describe('.formatId()', () => { 592 | it('coverts to lowercase', () => { 593 | expect(Contents.formatId('FOO')).to.equal('foo'); 594 | }); 595 | it('replaces characters with diacritics to their ASCII counterparts', () => { 596 | expect(Contents.formatId('ãàáäâẽèéëêìíïîõòóöôùúüûñç')).to.equal('aaaaaeeeeeiiiiooooouuuunc'); 597 | }); 598 | it('replaces whitespace with a dash', () => { 599 | expect(Contents.formatId('foo bar')).to.equal('foo-bar'); 600 | }); 601 | it('replaces sequences of characters outside /a-z0-9-_/ with a dash', () => { 602 | expect(Contents.formatId('a±!@#$%^&*b')).to.equal('a-b'); 603 | }); 604 | it('replaces multiple dashes with a single dash', () => { 605 | expect(Contents.formatId('a---b--c')).to.equal('a-b-c'); 606 | }); 607 | it('trims dashes from the beginning and end', () => { 608 | expect(Contents.formatId('---a---')).to.equal('a'); 609 | }); 610 | it('strips characters outside a-z from the beginning of the string', () => { 611 | expect(Contents.formatId('123!@#foo')).to.equal('foo'); 612 | }); 613 | }); 614 | }); 615 | 616 | describe('.config()', () => { 617 | const configFactory = (overwrite) => { 618 | const config = { 619 | ...overwrite 620 | }; 621 | 622 | return () => { 623 | return Contents.config(config); 624 | }; 625 | }; 626 | 627 | it('throws an error if an unknown property is provided', () => { 628 | expect(configFactory({unknown: null})).to.throw(Error, 'Unknown configuration property "unknown".'); 629 | }); 630 | describe('setting config.articles', () => { 631 | it('throws an error if it is not a collection of HTMLElement objects.', () => { 632 | expect(configFactory({articles: {}})).to.throw(Error, 'Option "articles" is not a collection of HTMLElement objects.'); 633 | }); 634 | }); 635 | describe('setting config.articleId', () => { 636 | it('throws an error if it is not a function', () => { 637 | expect(configFactory({articleId: 'not a function'})).to.throw(Error, 'Option "articleId" must be a function.'); 638 | }); 639 | it('defaults to Contents.articleId', () => { 640 | expect(configFactory()().articleId).to.equal(Contents.articleId); 641 | }); 642 | }); 643 | describe('setting config.articleName', () => { 644 | it('throws an error if it is not a function', () => { 645 | expect(configFactory({articleName: 'not a function'})).to.throw(Error, 'Option "articleName" must be a function.'); 646 | }); 647 | it('defaults to Contents.link', () => { 648 | expect(configFactory()().articleName).to.equal(Contents.articleName); 649 | }); 650 | }); 651 | describe('setting config.link', () => { 652 | it('throws an error if it is not a function', () => { 653 | expect(configFactory({link: 'not a function'})).to.throw(Error, 'Option "link" must be a function.'); 654 | }); 655 | it('defaults to Contents.link', () => { 656 | expect(configFactory()().link).to.equal(Contents.link); 657 | }); 658 | }); 659 | }); 660 | }); 661 | --------------------------------------------------------------------------------