tocbot.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077
  1. /******/ (() => { // webpackBootstrap
  2. /******/ var __webpack_modules__ = ({
  3. /***/ "./src/js/build-html.js":
  4. /*!******************************!*\
  5. !*** ./src/js/build-html.js ***!
  6. \******************************/
  7. /***/ ((module) => {
  8. /* eslint no-var: off */
  9. /**
  10. * This file is responsible for building the DOM and updating DOM state.
  11. *
  12. * @author Tim Scanlin
  13. */
  14. module.exports = function (options) {
  15. var forEach = [].forEach
  16. var some = [].some
  17. var body = document.body
  18. var tocElement
  19. var currentlyHighlighting = true
  20. var SPACE_CHAR = ' '
  21. /**
  22. * Create link and list elements.
  23. * @param {Object} d
  24. * @param {HTMLElement} container
  25. * @return {HTMLElement}
  26. */
  27. function createEl (d, container) {
  28. var link = container.appendChild(createLink(d))
  29. if (d.children.length) {
  30. var list = createList(d.isCollapsed)
  31. d.children.forEach(function (child) {
  32. createEl(child, list)
  33. })
  34. link.appendChild(list)
  35. }
  36. }
  37. /**
  38. * Render nested heading array data into a given element.
  39. * @param {HTMLElement} parent Optional. If provided updates the {@see tocElement} to match.
  40. * @param {Array} data
  41. * @return {HTMLElement}
  42. */
  43. function render (parent, data) {
  44. var collapsed = false
  45. var container = createList(collapsed)
  46. data.forEach(function (d) {
  47. createEl(d, container)
  48. })
  49. // Return if no TOC element is provided or known.
  50. tocElement = parent || tocElement
  51. if (tocElement === null) {
  52. return
  53. }
  54. // Remove existing child if it exists.
  55. if (tocElement.firstChild) {
  56. tocElement.removeChild(tocElement.firstChild)
  57. }
  58. // Just return the parent and don't append the list if no links are found.
  59. if (data.length === 0) {
  60. return tocElement
  61. }
  62. // Append the Elements that have been created
  63. return tocElement.appendChild(container)
  64. }
  65. /**
  66. * Create link element.
  67. * @param {Object} data
  68. * @return {HTMLElement}
  69. */
  70. function createLink (data) {
  71. var item = document.createElement('li')
  72. var a = document.createElement('a')
  73. if (options.listItemClass) {
  74. item.setAttribute('class', options.listItemClass)
  75. }
  76. if (options.onClick) {
  77. a.onclick = options.onClick
  78. }
  79. if (options.includeTitleTags) {
  80. a.setAttribute('title', data.textContent)
  81. }
  82. if (options.includeHtml && data.childNodes.length) {
  83. forEach.call(data.childNodes, function (node) {
  84. a.appendChild(node.cloneNode(true))
  85. })
  86. } else {
  87. // Default behavior. Set to textContent to keep tests happy.
  88. a.textContent = data.textContent
  89. }
  90. a.setAttribute('href', options.basePath + '#' + data.id)
  91. a.setAttribute('class', options.linkClass +
  92. SPACE_CHAR + 'node-name--' + data.nodeName +
  93. SPACE_CHAR + options.extraLinkClasses)
  94. item.appendChild(a)
  95. return item
  96. }
  97. /**
  98. * Create list element.
  99. * @param {Boolean} isCollapsed
  100. * @return {HTMLElement}
  101. */
  102. function createList (isCollapsed) {
  103. var listElement = (options.orderedList) ? 'ol' : 'ul'
  104. var list = document.createElement(listElement)
  105. var classes = options.listClass + SPACE_CHAR + options.extraListClasses
  106. if (isCollapsed) {
  107. // No plus/equals here fixes compilcation issue.
  108. classes = classes + SPACE_CHAR + options.collapsibleClass
  109. classes = classes + SPACE_CHAR + options.isCollapsedClass
  110. }
  111. list.setAttribute('class', classes)
  112. return list
  113. }
  114. /**
  115. * Update fixed sidebar class.
  116. * @return {HTMLElement}
  117. */
  118. function updateFixedSidebarClass () {
  119. if (options.scrollContainer && document.querySelector(options.scrollContainer)) {
  120. var top
  121. top = document.querySelector(options.scrollContainer).scrollTop
  122. } else {
  123. top = document.documentElement.scrollTop || body.scrollTop
  124. }
  125. var posFixedEl = document.querySelector(options.positionFixedSelector)
  126. if (options.fixedSidebarOffset === 'auto') {
  127. options.fixedSidebarOffset = tocElement.offsetTop
  128. }
  129. if (top > options.fixedSidebarOffset) {
  130. if (posFixedEl.className.indexOf(options.positionFixedClass) === -1) {
  131. posFixedEl.className += SPACE_CHAR + options.positionFixedClass
  132. }
  133. } else {
  134. posFixedEl.className = posFixedEl.className.replace(SPACE_CHAR + options.positionFixedClass, '')
  135. }
  136. }
  137. /**
  138. * Get top position of heading
  139. * @param {HTMLElement} obj
  140. * @return {int} position
  141. */
  142. function getHeadingTopPos (obj) {
  143. var position = 0
  144. if (obj !== null) {
  145. position = obj.offsetTop
  146. if (options.hasInnerContainers) { position += getHeadingTopPos(obj.offsetParent) }
  147. }
  148. return position
  149. }
  150. /**
  151. * Update className only when changed.
  152. * @param {HTMLElement} obj
  153. * @param {string} className
  154. * @return {HTMLElement} obj
  155. */
  156. function updateClassname (obj, className) {
  157. if (obj && obj.className !== className) {
  158. obj.className = className
  159. }
  160. return obj
  161. }
  162. /**
  163. * Update TOC highlighting and collapsed groupings.
  164. */
  165. function updateToc (headingsArray) {
  166. // If a fixed content container was set
  167. if (options.scrollContainer && document.querySelector(options.scrollContainer)) {
  168. var top
  169. top = document.querySelector(options.scrollContainer).scrollTop
  170. } else {
  171. top = document.documentElement.scrollTop || body.scrollTop
  172. }
  173. // Add fixed class at offset
  174. if (options.positionFixedSelector) {
  175. updateFixedSidebarClass()
  176. }
  177. // Get the top most heading currently visible on the page so we know what to highlight.
  178. var headings = headingsArray
  179. var topHeader
  180. // Using some instead of each so that we can escape early.
  181. if (currentlyHighlighting &&
  182. tocElement !== null &&
  183. headings.length > 0) {
  184. some.call(headings, function (heading, i) {
  185. if (getHeadingTopPos(heading) > top + options.headingsOffset + 10) {
  186. // Don't allow negative index value.
  187. var index = (i === 0) ? i : i - 1
  188. topHeader = headings[index]
  189. return true
  190. } else if (i === headings.length - 1) {
  191. // This allows scrolling for the last heading on the page.
  192. topHeader = headings[headings.length - 1]
  193. return true
  194. }
  195. })
  196. var oldActiveTocLink = tocElement.querySelector('.' + options.activeLinkClass)
  197. var activeTocLink = tocElement
  198. .querySelector('.' + options.linkClass +
  199. '.node-name--' + topHeader.nodeName +
  200. '[href="' + options.basePath + '#' + topHeader.id.replace(/([ #;&,.+*~':"!^$[\]()=>|/\\@])/g, '\\$1') + '"]')
  201. // Performance improvement to only change the classes
  202. // for the toc if a new link should be highlighted.
  203. if (oldActiveTocLink === activeTocLink) {
  204. return
  205. }
  206. // Remove the active class from the other tocLinks.
  207. var tocLinks = tocElement
  208. .querySelectorAll('.' + options.linkClass)
  209. forEach.call(tocLinks, function (tocLink) {
  210. updateClassname(tocLink, tocLink.className.replace(SPACE_CHAR + options.activeLinkClass, ''))
  211. })
  212. var tocLis = tocElement
  213. .querySelectorAll('.' + options.listItemClass)
  214. forEach.call(tocLis, function (tocLi) {
  215. updateClassname(tocLi, tocLi.className.replace(SPACE_CHAR + options.activeListItemClass, ''))
  216. })
  217. // Add the active class to the active tocLink.
  218. if (activeTocLink && activeTocLink.className.indexOf(options.activeLinkClass) === -1) {
  219. activeTocLink.className += SPACE_CHAR + options.activeLinkClass
  220. }
  221. var li = activeTocLink && activeTocLink.parentNode
  222. if (li && li.className.indexOf(options.activeListItemClass) === -1) {
  223. li.className += SPACE_CHAR + options.activeListItemClass
  224. }
  225. var tocLists = tocElement
  226. .querySelectorAll('.' + options.listClass + '.' + options.collapsibleClass)
  227. // Collapse the other collapsible lists.
  228. forEach.call(tocLists, function (list) {
  229. if (list.className.indexOf(options.isCollapsedClass) === -1) {
  230. list.className += SPACE_CHAR + options.isCollapsedClass
  231. }
  232. })
  233. // Expand the active link's collapsible list and its sibling if applicable.
  234. if (activeTocLink && activeTocLink.nextSibling && activeTocLink.nextSibling.className.indexOf(options.isCollapsedClass) !== -1) {
  235. updateClassname(activeTocLink.nextSibling, activeTocLink.nextSibling.className.replace(SPACE_CHAR + options.isCollapsedClass, ''))
  236. }
  237. removeCollapsedFromParents(activeTocLink && activeTocLink.parentNode.parentNode)
  238. }
  239. }
  240. /**
  241. * Remove collapsed class from parent elements.
  242. * @param {HTMLElement} element
  243. * @return {HTMLElement}
  244. */
  245. function removeCollapsedFromParents (element) {
  246. if (element && element.className.indexOf(options.collapsibleClass) !== -1 && element.className.indexOf(options.isCollapsedClass) !== -1) {
  247. updateClassname(element, element.className.replace(SPACE_CHAR + options.isCollapsedClass, ''))
  248. return removeCollapsedFromParents(element.parentNode.parentNode)
  249. }
  250. return element
  251. }
  252. /**
  253. * Disable TOC Animation when a link is clicked.
  254. * @param {Event} event
  255. */
  256. function disableTocAnimation (event) {
  257. var target = event.target || event.srcElement
  258. if (typeof target.className !== 'string' || target.className.indexOf(options.linkClass) === -1) {
  259. return
  260. }
  261. // Bind to tocLink clicks to temporarily disable highlighting
  262. // while smoothScroll is animating.
  263. currentlyHighlighting = false
  264. }
  265. /**
  266. * Enable TOC Animation.
  267. */
  268. function enableTocAnimation () {
  269. currentlyHighlighting = true
  270. }
  271. return {
  272. enableTocAnimation,
  273. disableTocAnimation,
  274. render,
  275. updateToc
  276. }
  277. }
  278. /***/ }),
  279. /***/ "./src/js/default-options.js":
  280. /*!***********************************!*\
  281. !*** ./src/js/default-options.js ***!
  282. \***********************************/
  283. /***/ ((module) => {
  284. module.exports = {
  285. // Where to render the table of contents.
  286. tocSelector: '.js-toc',
  287. // Where to grab the headings to build the table of contents.
  288. contentSelector: '.js-toc-content',
  289. // Which headings to grab inside of the contentSelector element.
  290. headingSelector: 'h1, h2, h3',
  291. // Headings that match the ignoreSelector will be skipped.
  292. ignoreSelector: '.js-toc-ignore',
  293. // For headings inside relative or absolute positioned containers within content
  294. hasInnerContainers: false,
  295. // Main class to add to links.
  296. linkClass: 'toc-link',
  297. // Extra classes to add to links.
  298. extraLinkClasses: '',
  299. // Class to add to active links,
  300. // the link corresponding to the top most heading on the page.
  301. activeLinkClass: 'is-active-link',
  302. // Main class to add to lists.
  303. listClass: 'toc-list',
  304. // Extra classes to add to lists.
  305. extraListClasses: '',
  306. // Class that gets added when a list should be collapsed.
  307. isCollapsedClass: 'is-collapsed',
  308. // Class that gets added when a list should be able
  309. // to be collapsed but isn't necessarily collapsed.
  310. collapsibleClass: 'is-collapsible',
  311. // Class to add to list items.
  312. listItemClass: 'toc-list-item',
  313. // Class to add to active list items.
  314. activeListItemClass: 'is-active-li',
  315. // How many heading levels should not be collapsed.
  316. // For example, number 6 will show everything since
  317. // there are only 6 heading levels and number 0 will collapse them all.
  318. // The sections that are hidden will open
  319. // and close as you scroll to headings within them.
  320. collapseDepth: 0,
  321. // Smooth scrolling enabled.
  322. scrollSmooth: true,
  323. // Smooth scroll duration.
  324. scrollSmoothDuration: 420,
  325. // Smooth scroll offset.
  326. scrollSmoothOffset: 0,
  327. // Callback for scroll end.
  328. scrollEndCallback: function (e) {},
  329. // Headings offset between the headings and the top of the document (this is meant for minor adjustments).
  330. headingsOffset: 1,
  331. // Timeout between events firing to make sure it's
  332. // not too rapid (for performance reasons).
  333. throttleTimeout: 50,
  334. // Element to add the positionFixedClass to.
  335. positionFixedSelector: null,
  336. // Fixed position class to add to make sidebar fixed after scrolling
  337. // down past the fixedSidebarOffset.
  338. positionFixedClass: 'is-position-fixed',
  339. // fixedSidebarOffset can be any number but by default is set
  340. // to auto which sets the fixedSidebarOffset to the sidebar
  341. // element's offsetTop from the top of the document on init.
  342. fixedSidebarOffset: 'auto',
  343. // includeHtml can be set to true to include the HTML markup from the
  344. // heading node instead of just including the innerText.
  345. includeHtml: false,
  346. // includeTitleTags automatically sets the html title tag of the link
  347. // to match the title. This can be useful for SEO purposes or
  348. // when truncating titles.
  349. includeTitleTags: false,
  350. // onclick function to apply to all links in toc. will be called with
  351. // the event as the first parameter, and this can be used to stop,
  352. // propagation, prevent default or perform action
  353. onClick: function (e) {},
  354. // orderedList can be set to false to generate unordered lists (ul)
  355. // instead of ordered lists (ol)
  356. orderedList: true,
  357. // If there is a fixed article scroll container, set to calculate titles' offset
  358. scrollContainer: null,
  359. // prevent ToC DOM rendering if it's already rendered by an external system
  360. skipRendering: false,
  361. // Optional callback to change heading labels.
  362. // For example it can be used to cut down and put ellipses on multiline headings you deem too long.
  363. // Called each time a heading is parsed. Expects a string and returns the modified label to display.
  364. // Additionally, the attribute `data-heading-label` may be used on a heading to specify
  365. // a shorter string to be used in the TOC.
  366. // function (string) => string
  367. headingLabelCallback: false,
  368. // ignore headings that are hidden in DOM
  369. ignoreHiddenElements: false,
  370. // Optional callback to modify properties of parsed headings.
  371. // The heading element is passed in node parameter and information parsed by default parser is provided in obj parameter.
  372. // Function has to return the same or modified obj.
  373. // The heading will be excluded from TOC if nothing is returned.
  374. // function (object, HTMLElement) => object | void
  375. headingObjectCallback: null,
  376. // Set the base path, useful if you use a `base` tag in `head`.
  377. basePath: '',
  378. // Only takes affect when `tocSelector` is scrolling,
  379. // keep the toc scroll position in sync with the content.
  380. disableTocScrollSync: false,
  381. // Offset for the toc scroll (top) position when scrolling the page.
  382. // Only effective if `disableTocScrollSync` is false.
  383. tocScrollOffset: 0
  384. }
  385. /***/ }),
  386. /***/ "./src/js/index.js":
  387. /*!*************************!*\
  388. !*** ./src/js/index.js ***!
  389. \*************************/
  390. /***/ ((module, exports, __webpack_require__) => {
  391. var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/* eslint no-var: off */
  392. /**
  393. * Tocbot
  394. * Tocbot creates a table of contents based on HTML headings on a page,
  395. * this allows users to easily jump to different sections of the document.
  396. * Tocbot was inspired by tocify (http://gregfranko.com/jquery.tocify.js/).
  397. * The main differences are that it works natively without any need for jquery or jquery UI).
  398. *
  399. * @author Tim Scanlin
  400. */
  401. /* globals define */
  402. (function (root, factory) {
  403. if (true) {
  404. !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory(root)),
  405. __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?
  406. (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__),
  407. __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__))
  408. } else {}
  409. })(typeof __webpack_require__.g !== 'undefined' ? __webpack_require__.g : window || __webpack_require__.g, function (root) {
  410. 'use strict'
  411. // Default options.
  412. var defaultOptions = __webpack_require__(/*! ./default-options.js */ "./src/js/default-options.js")
  413. // Object to store current options.
  414. var options = {}
  415. // Object for public APIs.
  416. var tocbot = {}
  417. var BuildHtml = __webpack_require__(/*! ./build-html.js */ "./src/js/build-html.js")
  418. var ParseContent = __webpack_require__(/*! ./parse-content.js */ "./src/js/parse-content.js")
  419. var updateTocScroll = __webpack_require__(/*! ./update-toc-scroll.js */ "./src/js/update-toc-scroll.js")
  420. // Keep these variables at top scope once options are passed in.
  421. var buildHtml
  422. var parseContent
  423. // Just return if its not a browser.
  424. var supports = !!root && !!root.document && !!root.document.querySelector && !!root.addEventListener // Feature test
  425. if (typeof window === 'undefined' && !supports) {
  426. return
  427. }
  428. var headingsArray
  429. // From: https://github.com/Raynos/xtend
  430. var hasOwnProperty = Object.prototype.hasOwnProperty
  431. function extend () {
  432. var target = {}
  433. for (var i = 0; i < arguments.length; i++) {
  434. var source = arguments[i]
  435. for (var key in source) {
  436. if (hasOwnProperty.call(source, key)) {
  437. target[key] = source[key]
  438. }
  439. }
  440. }
  441. return target
  442. }
  443. // From: https://remysharp.com/2010/07/21/throttling-function-calls
  444. function throttle (fn, threshold, scope) {
  445. threshold || (threshold = 250)
  446. var last
  447. var deferTimer
  448. return function () {
  449. var context = scope || this
  450. var now = +new Date()
  451. var args = arguments
  452. if (last && now < last + threshold) {
  453. // hold on to it
  454. clearTimeout(deferTimer)
  455. deferTimer = setTimeout(function () {
  456. last = now
  457. fn.apply(context, args)
  458. }, threshold)
  459. } else {
  460. last = now
  461. fn.apply(context, args)
  462. }
  463. }
  464. }
  465. function getContentElement (options) {
  466. try {
  467. return options.contentElement || document.querySelector(options.contentSelector)
  468. } catch (e) {
  469. console.warn('Contents element not found: ' + options.contentSelector) // eslint-disable-line
  470. return null
  471. }
  472. }
  473. function getTocElement (options) {
  474. try {
  475. return options.tocElement || document.querySelector(options.tocSelector)
  476. } catch (e) {
  477. console.warn('TOC element not found: ' + options.tocSelector) // eslint-disable-line
  478. return null
  479. }
  480. }
  481. /**
  482. * Destroy tocbot.
  483. */
  484. tocbot.destroy = function () {
  485. var tocElement = getTocElement(options)
  486. if (tocElement === null) {
  487. return
  488. }
  489. if (!options.skipRendering) {
  490. // Clear HTML.
  491. if (tocElement) {
  492. tocElement.innerHTML = ''
  493. }
  494. }
  495. // Remove event listeners.
  496. if (options.scrollContainer && document.querySelector(options.scrollContainer)) {
  497. document.querySelector(options.scrollContainer).removeEventListener('scroll', this._scrollListener, false)
  498. document.querySelector(options.scrollContainer).removeEventListener('resize', this._scrollListener, false)
  499. if (buildHtml) {
  500. document.querySelector(options.scrollContainer).removeEventListener('click', this._clickListener, false)
  501. }
  502. } else {
  503. document.removeEventListener('scroll', this._scrollListener, false)
  504. document.removeEventListener('resize', this._scrollListener, false)
  505. if (buildHtml) {
  506. document.removeEventListener('click', this._clickListener, false)
  507. }
  508. }
  509. }
  510. /**
  511. * Initialize tocbot.
  512. * @param {object} customOptions
  513. */
  514. tocbot.init = function (customOptions) {
  515. // feature test
  516. if (!supports) {
  517. return
  518. }
  519. // Merge defaults with user options.
  520. // Set to options variable at the top.
  521. options = extend(defaultOptions, customOptions || {})
  522. this.options = options
  523. this.state = {}
  524. // Init smooth scroll if enabled (default).
  525. if (options.scrollSmooth) {
  526. options.duration = options.scrollSmoothDuration
  527. options.offset = options.scrollSmoothOffset
  528. tocbot.scrollSmooth = (__webpack_require__(/*! ./scroll-smooth */ "./src/js/scroll-smooth/index.js").initSmoothScrolling)(options)
  529. }
  530. // Pass options to these modules.
  531. buildHtml = BuildHtml(options)
  532. parseContent = ParseContent(options)
  533. // For testing purposes.
  534. this._buildHtml = buildHtml
  535. this._parseContent = parseContent
  536. this._headingsArray = headingsArray
  537. // Destroy it if it exists first.
  538. tocbot.destroy()
  539. var contentElement = getContentElement(options)
  540. if (contentElement === null) {
  541. return
  542. }
  543. var tocElement = getTocElement(options)
  544. if (tocElement === null) {
  545. return
  546. }
  547. // Get headings array.
  548. headingsArray = parseContent.selectHeadings(contentElement, options.headingSelector)
  549. // Return if no headings are found.
  550. if (headingsArray === null) {
  551. return
  552. }
  553. // Build nested headings array.
  554. var nestedHeadingsObj = parseContent.nestHeadingsArray(headingsArray)
  555. var nestedHeadings = nestedHeadingsObj.nest
  556. // Render.
  557. if (!options.skipRendering) {
  558. buildHtml.render(tocElement, nestedHeadings)
  559. } else {
  560. // No need to attach listeners if skipRendering is true, this was causing errors.
  561. return this
  562. }
  563. // Update Sidebar and bind listeners.
  564. this._scrollListener = throttle(function (e) {
  565. buildHtml.updateToc(headingsArray)
  566. !options.disableTocScrollSync && updateTocScroll(options)
  567. var isTop = e && e.target && e.target.scrollingElement && e.target.scrollingElement.scrollTop === 0
  568. if ((e && (e.eventPhase === 0 || e.currentTarget === null)) || isTop) {
  569. buildHtml.updateToc(headingsArray)
  570. if (options.scrollEndCallback) {
  571. options.scrollEndCallback(e)
  572. }
  573. }
  574. }, options.throttleTimeout)
  575. this._scrollListener()
  576. if (options.scrollContainer && document.querySelector(options.scrollContainer)) {
  577. document.querySelector(options.scrollContainer).addEventListener('scroll', this._scrollListener, false)
  578. document.querySelector(options.scrollContainer).addEventListener('resize', this._scrollListener, false)
  579. } else {
  580. document.addEventListener('scroll', this._scrollListener, false)
  581. document.addEventListener('resize', this._scrollListener, false)
  582. }
  583. // Bind click listeners to disable animation.
  584. var timeout = null
  585. this._clickListener = throttle(function (event) {
  586. if (options.scrollSmooth) {
  587. buildHtml.disableTocAnimation(event)
  588. }
  589. buildHtml.updateToc(headingsArray)
  590. // Timeout to re-enable the animation.
  591. timeout && clearTimeout(timeout)
  592. timeout = setTimeout(function () {
  593. buildHtml.enableTocAnimation()
  594. }, options.scrollSmoothDuration)
  595. }, options.throttleTimeout)
  596. if (options.scrollContainer && document.querySelector(options.scrollContainer)) {
  597. document.querySelector(options.scrollContainer).addEventListener('click', this._clickListener, false)
  598. } else {
  599. document.addEventListener('click', this._clickListener, false)
  600. }
  601. return this
  602. }
  603. /**
  604. * Refresh tocbot.
  605. */
  606. tocbot.refresh = function (customOptions) {
  607. tocbot.destroy()
  608. tocbot.init(customOptions || this.options)
  609. }
  610. // Make tocbot available globally.
  611. root.tocbot = tocbot
  612. return tocbot
  613. })
  614. /***/ }),
  615. /***/ "./src/js/parse-content.js":
  616. /*!*********************************!*\
  617. !*** ./src/js/parse-content.js ***!
  618. \*********************************/
  619. /***/ ((module) => {
  620. /* eslint no-var: off */
  621. /**
  622. * This file is responsible for parsing the content from the DOM and making
  623. * sure data is nested properly.
  624. *
  625. * @author Tim Scanlin
  626. */
  627. module.exports = function parseContent (options) {
  628. var reduce = [].reduce
  629. /**
  630. * Get the last item in an array and return a reference to it.
  631. * @param {Array} array
  632. * @return {Object}
  633. */
  634. function getLastItem (array) {
  635. return array[array.length - 1]
  636. }
  637. /**
  638. * Get heading level for a heading dom node.
  639. * @param {HTMLElement} heading
  640. * @return {Number}
  641. */
  642. function getHeadingLevel (heading) {
  643. return +heading.nodeName.toUpperCase().replace('H', '')
  644. }
  645. /**
  646. * Determine whether the object is an HTML Element.
  647. * Also works inside iframes. HTML Elements might be created by the parent document.
  648. * @param {Object} maybeElement
  649. * @return {Number}
  650. */
  651. function isHTMLElement (maybeElement) {
  652. try {
  653. return (
  654. maybeElement instanceof window.HTMLElement ||
  655. maybeElement instanceof window.parent.HTMLElement
  656. )
  657. } catch (e) {
  658. return maybeElement instanceof window.HTMLElement
  659. }
  660. }
  661. /**
  662. * Get important properties from a heading element and store in a plain object.
  663. * @param {HTMLElement} heading
  664. * @return {Object}
  665. */
  666. function getHeadingObject (heading) {
  667. // each node is processed twice by this method because nestHeadingsArray() and addNode() calls it
  668. // first time heading is real DOM node element, second time it is obj
  669. // that is causing problem so I am processing only original DOM node
  670. if (!isHTMLElement(heading)) return heading
  671. if (options.ignoreHiddenElements && (!heading.offsetHeight || !heading.offsetParent)) {
  672. return null
  673. }
  674. const headingLabel = heading.getAttribute('data-heading-label') ||
  675. (options.headingLabelCallback ? String(options.headingLabelCallback(heading.innerText)) : (heading.innerText || heading.textContent).trim())
  676. var obj = {
  677. id: heading.id,
  678. children: [],
  679. nodeName: heading.nodeName,
  680. headingLevel: getHeadingLevel(heading),
  681. textContent: headingLabel
  682. }
  683. if (options.includeHtml) {
  684. obj.childNodes = heading.childNodes
  685. }
  686. if (options.headingObjectCallback) {
  687. return options.headingObjectCallback(obj, heading)
  688. }
  689. return obj
  690. }
  691. /**
  692. * Add a node to the nested array.
  693. * @param {Object} node
  694. * @param {Array} nest
  695. * @return {Array}
  696. */
  697. function addNode (node, nest) {
  698. var obj = getHeadingObject(node)
  699. var level = obj.headingLevel
  700. var array = nest
  701. var lastItem = getLastItem(array)
  702. var lastItemLevel = lastItem
  703. ? lastItem.headingLevel
  704. : 0
  705. var counter = level - lastItemLevel
  706. while (counter > 0) {
  707. lastItem = getLastItem(array)
  708. // Handle case where there are multiple h5+ in a row.
  709. if (lastItem && level === lastItem.headingLevel) {
  710. break
  711. } else if (lastItem && lastItem.children !== undefined) {
  712. array = lastItem.children
  713. }
  714. counter--
  715. }
  716. if (level >= options.collapseDepth) {
  717. obj.isCollapsed = true
  718. }
  719. array.push(obj)
  720. return array
  721. }
  722. /**
  723. * Select headings in content area, exclude any selector in options.ignoreSelector
  724. * @param {HTMLElement} contentElement
  725. * @param {Array} headingSelector
  726. * @return {Array}
  727. */
  728. function selectHeadings (contentElement, headingSelector) {
  729. var selectors = headingSelector
  730. if (options.ignoreSelector) {
  731. selectors = headingSelector.split(',')
  732. .map(function mapSelectors (selector) {
  733. return selector.trim() + ':not(' + options.ignoreSelector + ')'
  734. })
  735. }
  736. try {
  737. return contentElement.querySelectorAll(selectors)
  738. } catch (e) {
  739. console.warn('Headers not found with selector: ' + selectors); // eslint-disable-line
  740. return null
  741. }
  742. }
  743. /**
  744. * Nest headings array into nested arrays with 'children' property.
  745. * @param {Array} headingsArray
  746. * @return {Object}
  747. */
  748. function nestHeadingsArray (headingsArray) {
  749. return reduce.call(headingsArray, function reducer (prev, curr) {
  750. var currentHeading = getHeadingObject(curr)
  751. if (currentHeading) {
  752. addNode(currentHeading, prev.nest)
  753. }
  754. return prev
  755. }, {
  756. nest: []
  757. })
  758. }
  759. return {
  760. nestHeadingsArray,
  761. selectHeadings
  762. }
  763. }
  764. /***/ }),
  765. /***/ "./src/js/scroll-smooth/index.js":
  766. /*!***************************************!*\
  767. !*** ./src/js/scroll-smooth/index.js ***!
  768. \***************************************/
  769. /***/ ((__unused_webpack_module, exports) => {
  770. /* eslint no-var: off */
  771. /* globals location, requestAnimationFrame */
  772. exports.initSmoothScrolling = initSmoothScrolling
  773. function initSmoothScrolling (options) {
  774. // if (isCssSmoothSCrollSupported()) { return }
  775. var duration = options.duration
  776. var offset = options.offset
  777. var pageUrl = location.hash
  778. ? stripHash(location.href)
  779. : location.href
  780. delegatedLinkHijacking()
  781. function delegatedLinkHijacking () {
  782. document.body.addEventListener('click', onClick, false)
  783. function onClick (e) {
  784. if (
  785. !isInPageLink(e.target) ||
  786. e.target.className.indexOf('no-smooth-scroll') > -1 ||
  787. (e.target.href.charAt(e.target.href.length - 2) === '#' &&
  788. e.target.href.charAt(e.target.href.length - 1) === '!') ||
  789. e.target.className.indexOf(options.linkClass) === -1) {
  790. return
  791. }
  792. // Don't prevent default or hash doesn't change.
  793. // e.preventDefault()
  794. jump(e.target.hash, {
  795. duration,
  796. offset,
  797. callback: function () {
  798. setFocus(e.target.hash)
  799. }
  800. })
  801. }
  802. }
  803. function isInPageLink (n) {
  804. return n.tagName.toLowerCase() === 'a' &&
  805. (n.hash.length > 0 || n.href.charAt(n.href.length - 1) === '#') &&
  806. (stripHash(n.href) === pageUrl || stripHash(n.href) + '#' === pageUrl)
  807. }
  808. function stripHash (url) {
  809. return url.slice(0, url.lastIndexOf('#'))
  810. }
  811. // function isCssSmoothSCrollSupported () {
  812. // return 'scrollBehavior' in document.documentElement.style
  813. // }
  814. // Adapted from:
  815. // https://www.nczonline.net/blog/2013/01/15/fixing-skip-to-content-links/
  816. function setFocus (hash) {
  817. var element = document.getElementById(hash.substring(1))
  818. if (element) {
  819. if (!/^(?:a|select|input|button|textarea)$/i.test(element.tagName)) {
  820. element.tabIndex = -1
  821. }
  822. element.focus()
  823. }
  824. }
  825. }
  826. function jump (target, options) {
  827. var start = window.pageYOffset
  828. var opt = {
  829. duration: options.duration,
  830. offset: options.offset || 0,
  831. callback: options.callback,
  832. easing: options.easing || easeInOutQuad
  833. }
  834. // This makes ids that start with a number work: ('[id="' + decodeURI(target).split('#').join('') + '"]')
  835. // DecodeURI for nonASCII hashes, they was encoded, but id was not encoded, it lead to not finding the tgt element by id.
  836. // And this is for IE: document.body.scrollTop
  837. // Handle decoded and non-decoded URIs since sometimes URLs automatically transform them (support for internation chars).
  838. var tgt = document.querySelector('[id="' + decodeURI(target).split('#').join('') + '"]') ||
  839. document.querySelector('[id="' + (target).split('#').join('') + '"]')
  840. var distance = typeof target === 'string'
  841. ? opt.offset + (
  842. target
  843. ? (tgt && tgt.getBoundingClientRect().top) || 0 // handle non-existent links better.
  844. : -(document.documentElement.scrollTop || document.body.scrollTop))
  845. : target
  846. var duration = typeof opt.duration === 'function'
  847. ? opt.duration(distance)
  848. : opt.duration
  849. var timeStart
  850. var timeElapsed
  851. requestAnimationFrame(function (time) { timeStart = time; loop(time) })
  852. function loop (time) {
  853. timeElapsed = time - timeStart
  854. window.scrollTo(0, opt.easing(timeElapsed, start, distance, duration))
  855. if (timeElapsed < duration) { requestAnimationFrame(loop) } else { end() }
  856. }
  857. function end () {
  858. window.scrollTo(0, start + distance)
  859. if (typeof opt.callback === 'function') { opt.callback() }
  860. }
  861. // Robert Penner's easeInOutQuad - http://robertpenner.com/easing/
  862. function easeInOutQuad (t, b, c, d) {
  863. t /= d / 2
  864. if (t < 1) return c / 2 * t * t + b
  865. t--
  866. return -c / 2 * (t * (t - 2) - 1) + b
  867. }
  868. }
  869. /***/ }),
  870. /***/ "./src/js/update-toc-scroll.js":
  871. /*!*************************************!*\
  872. !*** ./src/js/update-toc-scroll.js ***!
  873. \*************************************/
  874. /***/ ((module) => {
  875. /* eslint no-var: off */
  876. const SCROLL_LEEWAY = 30
  877. module.exports = function updateTocScroll (options) {
  878. var toc = options.tocElement || document.querySelector(options.tocSelector)
  879. if (toc && toc.scrollHeight > toc.clientHeight) {
  880. var activeItem = toc.querySelector('.' + options.activeListItemClass)
  881. if (activeItem) {
  882. // Determine container top and bottom
  883. var cTop = toc.scrollTop
  884. var cBottom = cTop + toc.clientHeight
  885. // Determine element top and bottom
  886. var eTop = activeItem.offsetTop
  887. var eBottom = eTop + activeItem.clientHeight
  888. // Check if out of view
  889. // Above scroll view
  890. if (eTop < cTop + options.tocScrollOffset) {
  891. toc.scrollTop -= (cTop - eTop) + options.tocScrollOffset
  892. // Below scroll view
  893. } else if (eBottom > cBottom - options.tocScrollOffset - SCROLL_LEEWAY) {
  894. toc.scrollTop += (eBottom - cBottom) + options.tocScrollOffset + (2 * SCROLL_LEEWAY)
  895. }
  896. }
  897. }
  898. }
  899. /***/ })
  900. /******/ });
  901. /************************************************************************/
  902. /******/ // The module cache
  903. /******/ var __webpack_module_cache__ = {};
  904. /******/
  905. /******/ // The require function
  906. /******/ function __webpack_require__(moduleId) {
  907. /******/ // Check if module is in cache
  908. /******/ var cachedModule = __webpack_module_cache__[moduleId];
  909. /******/ if (cachedModule !== undefined) {
  910. /******/ return cachedModule.exports;
  911. /******/ }
  912. /******/ // Create a new module (and put it into the cache)
  913. /******/ var module = __webpack_module_cache__[moduleId] = {
  914. /******/ // no module.id needed
  915. /******/ // no module.loaded needed
  916. /******/ exports: {}
  917. /******/ };
  918. /******/
  919. /******/ // Execute the module function
  920. /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
  921. /******/
  922. /******/ // Return the exports of the module
  923. /******/ return module.exports;
  924. /******/ }
  925. /******/
  926. /************************************************************************/
  927. /******/ /* webpack/runtime/global */
  928. /******/ (() => {
  929. /******/ __webpack_require__.g = (function() {
  930. /******/ if (typeof globalThis === 'object') return globalThis;
  931. /******/ try {
  932. /******/ return this || new Function('return this')();
  933. /******/ } catch (e) {
  934. /******/ if (typeof window === 'object') return window;
  935. /******/ }
  936. /******/ })();
  937. /******/ })();
  938. /******/
  939. /************************************************************************/
  940. /******/
  941. /******/ // startup
  942. /******/ // Load entry module and return exports
  943. /******/ // This entry module used 'module' so it can't be inlined
  944. /******/ var __webpack_exports__ = __webpack_require__("./src/js/index.js");
  945. /******/
  946. /******/ })()
  947. ;
  948. //# sourceMappingURL=main.js.map