UNPKG

dom-minimap

Version:
179 lines (159 loc) 5.62 kB
const yo = require('yo-yo') const onload = require('on-load') const nanoraf = require('nanoraf') const insertCss = require('insert-css') const debounce = require('lodash.debounce') insertCss(` .dom-minimap-section { position: absolute; background-color: lightgrey; overflow: hidden; color: grey; font-size: 11px; padding-left: 2px; border-radius: 2px; -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; cursor: pointer; left: 5px; right: 5px; } .dom-minimap-section:hover { background-color: #e6e6e6; } .dom-minimap-scroll { pointer-events: none; position: absolute; background-color: rgba(0,0,0,0.15); top: 0; left: 0; right: 0; bottom: 0; } `) module.exports = minimap function minimap (opts) { opts = opts || {} opts.sections = opts.sections || 'minimap-section' if (typeof opts.sections !== 'function') { var sectionName = opts.sections opts.sections = (container) => Array.prototype.slice.call(container.getElementsByClassName(sectionName)) } opts.title = opts.title || 'data-section-title' if (typeof opts.title !== 'function') { var titleName = opts.title opts.title = (section) => section.getAttribute(titleName) } opts.tooltip = opts.tooltip || 'data-section-tooltip' if (typeof opts.hover !== 'function') { var tooltipName = opts.tooltip opts.tooltip = (section) => section.getAttribute(tooltipName) } opts.content = opts.content || 'minimap-content' opts.mapStyle = typeof opts.mapStyle !== 'undefined' ? opts.mapStyle : 'height: 100%;' opts.sectionStyle = opts.sectionStyle || '' opts.clickOffset = opts.clickOffset || 0 var lastContainerHeight var container var element = document.createElement('div') element.style.flex = '1' const render = nanoraf(renderMap) var state = { opts: opts } onload(element, function load () { container = typeof opts.content === 'string' ? document.getElementById(opts.content) : opts.content lastContainerHeight = container.scrollHeight // update on scroll event container.addEventListener('scroll', scrollUpdate) // update on window resize event window.addEventListener('resize', debounce(update), 100) // update on element loaded update() }, null, minimap) element.addEventListener('wheel', function (event) { if (container) container.scrollTop = container.scrollTop + event.deltaY }) function scrollUpdate () { state.scroll = getScroll(container) var top = element.querySelector('.dom-minimap-scroll-top') var bottom = element.querySelector('.dom-minimap-scroll-bottom') if (!top || !bottom) return top.style.bottom = state.scroll.topFromBottom bottom.style.top = state.scroll.bottomFromTop } function update () { var sections = getSections(container, element, opts) if (!sections) return var newState = Object.assign({}, state, { sections: sections, scroll: getScroll(container) }) render(newState, state) state = newState } return function () { if (container) { setTimeout(function () { if (lastContainerHeight !== container.scrollHeight) { update() } }, 1) } return element } function scrollTo () { var top = this.style.top.slice(0, -1) if (top) container.scrollTop = Math.round((container.scrollHeight * top) / 100) + opts.clickOffset } function renderMap (state) { var content = yo`<div style="margin-top:20px;text-align:center">loading</div>` if (state.sections) { content = state.sections.map((section) => { return yo` <div class="dom-minimap-section unselectable" title=${section.tooltip} onclick=${scrollTo} style="top:${section.top};bottom:${section.bottom};${( typeof opts.sectionStyle === 'function' ? opts.sectionStyle(section) : opts.sectionStyle )}"> ${section.title} </div> ` }).concat([ yo`<div class="dom-minimap-scroll dom-minimap-scroll-top" style="bottom:${state.scroll.topFromBottom}"></div>`, yo`<div class="dom-minimap-scroll dom-minimap-scroll-bottom" style="top:${state.scroll.bottomFromTop}"></div>` ]) } yo.update(element, yo`<div style='position:relative;${opts.mapStyle}'>${content}</div>`) } } function getScroll (container) { var top = container.scrollTop var cHeight = container.clientHeight var height = container.scrollHeight return { topFromBottom: ((height - top) / height * 100) + '%', bottomFromTop: ((1 - ((height - top - cHeight) / height)) * 100) + '%' } } function getSections (content, map, opts) { if (!map.parentElement) return false var cHeight = content.scrollHeight var mHeight = map.parentElement.clientHeight var cBounds = content.getBoundingClientRect() var scrollTop = content.scrollTop return opts.sections(content).map((section) => { var bounds = section.getBoundingClientRect() var top = (bounds.top - cBounds.top + scrollTop) / cHeight var bottom = (bounds.bottom - cBounds.top + scrollTop) / cHeight return { top: top * 100 + '%', bottom: applyPadding((1 - bottom) * 100 + '%', opts.paddingBottom), title: opts.title(section, (mHeight * bottom) - mHeight * top), tooltip: opts.tooltip(section), element: section } }) } function applyPadding (value, padding) { if (!padding) return value return `calc(${value} + ${padding})` }