UNPKG

bootstrap-vue

Version:

BootstrapVue provides one of the most comprehensive implementations of Bootstrap 4 components and grid system for Vue.js and with extensive and automated WAI-ARIA accessibility markup.

479 lines (428 loc) 14.8 kB
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } /* * ScrollSpy class definition */ import { assign } from '../../utils/object'; import observeDom from '../../utils/observe-dom'; import warn from '../../utils/warn'; import { isElement, isVisible, closest, matches, getBCR, offset, position, selectAll, select, hasClass, addClass, removeClass, getAttr, eventOn, eventOff } from '../../utils/dom'; /* * Constants / Defaults */ var NAME = 'v-b-scrollspy'; var ACTIVATE_EVENT = 'bv::scrollspy::activate'; var Default = { element: 'body', offset: 10, method: 'auto', throttle: 75 }; var DefaultType = { element: '(string|element|component)', offset: 'number', method: 'string', throttle: 'number' }; var ClassName = { DROPDOWN_ITEM: 'dropdown-item', ACTIVE: 'active' }; var Selector = { ACTIVE: '.active', NAV_LIST_GROUP: '.nav, .list-group', NAV_LINKS: '.nav-link', NAV_ITEMS: '.nav-item', LIST_ITEMS: '.list-group-item', DROPDOWN: '.dropdown, .dropup', DROPDOWN_ITEMS: '.dropdown-item', DROPDOWN_TOGGLE: '.dropdown-toggle' }; var OffsetMethod = { OFFSET: 'offset', POSITION: 'position' // HREFs must start with # but can be === '#', or start with '#/' or '#!' (which can be router links) };var HREF_REGEX = /^#[^/!]+/; // Transition Events var TransitionEndEvents = ['webkitTransitionEnd', 'transitionend', 'otransitionend', 'oTransitionEnd']; /* * Utility Methods */ // Better var type detection /* istanbul ignore next: not easy to test */ function toType(obj) { return {}.toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase(); } // Check config properties for expected types /* istanbul ignore next: not easy to test */ function typeCheckConfig(componentName, config, configTypes) { for (var property in configTypes) { if (Object.prototype.hasOwnProperty.call(configTypes, property)) { var expectedTypes = configTypes[property]; var value = config[property]; var valueType = value && isElement(value) ? 'element' : toType(value); // handle Vue instances valueType = value && value._isVue ? 'component' : valueType; if (!new RegExp(expectedTypes).test(valueType)) { warn(componentName + ': Option "' + property + '" provided type "' + valueType + '", but expected type "' + expectedTypes + '"'); } } } } /* * ------------------------------------------------------------------------ * Class Definition * ------------------------------------------------------------------------ */ /* istanbul ignore next: not easy to test */ var ScrollSpy = function () { function ScrollSpy(element, config, $root) { _classCallCheck(this, ScrollSpy); // The element we activate links in this.$el = element; this.$scroller = null; this.$selector = [Selector.NAV_LINKS, Selector.LIST_ITEMS, Selector.DROPDOWN_ITEMS].join(','); this.$offsets = []; this.$targets = []; this.$activeTarget = null; this.$scrollHeight = 0; this.$resizeTimeout = null; this.$obs_scroller = null; this.$obs_targets = null; this.$root = $root || null; this.$config = null; this.updateConfig(config); } _createClass(ScrollSpy, [{ key: 'updateConfig', value: function updateConfig(config, $root) { if (this.$scroller) { // Just in case out scroll element has changed this.unlisten(); this.$scroller = null; } var cfg = assign({}, this.constructor.Default, config); if ($root) { this.$root = $root; } typeCheckConfig(this.constructor.Name, cfg, this.constructor.DefaultType); this.$config = cfg; if (this.$root) { var self = this; this.$root.$nextTick(function () { self.listen(); }); } else { this.listen(); } } }, { key: 'dispose', value: function dispose() { this.unlisten(); clearTimeout(this.$resizeTimeout); this.$resizeTimeout = null; this.$el = null; this.$config = null; this.$scroller = null; this.$selector = null; this.$offsets = null; this.$targets = null; this.$activeTarget = null; this.$scrollHeight = null; } }, { key: 'listen', value: function listen() { var _this = this; var scroller = this.getScroller(); if (scroller && scroller.tagName !== 'BODY') { eventOn(scroller, 'scroll', this); } eventOn(window, 'scroll', this); eventOn(window, 'resize', this); eventOn(window, 'orientationchange', this); TransitionEndEvents.forEach(function (evtName) { eventOn(window, evtName, _this); }); this.setObservers(true); // Scedule a refresh this.handleEvent('refresh'); } }, { key: 'unlisten', value: function unlisten() { var _this2 = this; var scroller = this.getScroller(); this.setObservers(false); if (scroller && scroller.tagName !== 'BODY') { eventOff(scroller, 'scroll', this); } eventOff(window, 'scroll', this); eventOff(window, 'resize', this); eventOff(window, 'orientationchange', this); TransitionEndEvents.forEach(function (evtName) { eventOff(window, evtName, _this2); }); } }, { key: 'setObservers', value: function setObservers(on) { var _this3 = this; // We observe both the scroller for content changes, and the target links if (this.$obs_scroller) { this.$obs_scroller.disconnect(); this.$obs_scroller = null; } if (this.$obs_targets) { this.$obs_targets.disconnect(); this.$obs_targets = null; } if (on) { this.$obs_targets = observeDom(this.$el, function () { _this3.handleEvent('mutation'); }, { subtree: true, childList: true, attributes: true, attributeFilter: ['href'] }); this.$obs_scroller = observeDom(this.getScroller(), function () { _this3.handleEvent('mutation'); }, { subtree: true, childList: true, characterData: true, attributes: true, attributeFilter: ['id', 'style', 'class'] }); } } // general event handler }, { key: 'handleEvent', value: function handleEvent(evt) { var type = typeof evt === 'string' ? evt : evt.type; var self = this; function resizeThrottle() { if (!self.$resizeTimeout) { self.$resizeTimeout = setTimeout(function () { self.refresh(); self.process(); self.$resizeTimeout = null; }, self.$config.throttle); } } if (type === 'scroll') { if (!this.$obs_scroller) { // Just in case we are added to the DOM before the scroll target is // We re-instantiate our listeners, just in case this.listen(); } this.process(); } else if (/(resize|orientationchange|mutation|refresh)/.test(type)) { // Postpone these events by throttle time resizeThrottle(); } } // Refresh the list of target links on the element we are applied to }, { key: 'refresh', value: function refresh() { var _this4 = this; var scroller = this.getScroller(); if (!scroller) { return; } var autoMethod = scroller !== scroller.window ? OffsetMethod.POSITION : OffsetMethod.OFFSET; var method = this.$config.method === 'auto' ? autoMethod : this.$config.method; var methodFn = method === OffsetMethod.POSITION ? position : offset; var offsetBase = method === OffsetMethod.POSITION ? this.getScrollTop() : 0; this.$offsets = []; this.$targets = []; this.$scrollHeight = this.getScrollHeight(); // Find all the unique link href's selectAll(this.$selector, this.$el).map(function (link) { return getAttr(link, 'href'); }).filter(function (href) { return HREF_REGEX.test(href || ''); }).map(function (href) { var el = select(href, scroller); if (isVisible(el)) { return { offset: parseInt(methodFn(el).top, 10) + offsetBase, target: href }; } return null; }).filter(function (item) { return item; }).sort(function (a, b) { return a.offset - b.offset; }).reduce(function (memo, item) { // record only unique targets/offfsets if (!memo[item.target]) { _this4.$offsets.push(item.offset); _this4.$targets.push(item.target); memo[item.target] = true; } return memo; }, {}); return this; } // Handle activating/clearing }, { key: 'process', value: function process() { var scrollTop = this.getScrollTop() + this.$config.offset; var scrollHeight = this.getScrollHeight(); var maxScroll = this.$config.offset + scrollHeight - this.getOffsetHeight(); if (this.$scrollHeight !== scrollHeight) { this.refresh(); } if (scrollTop >= maxScroll) { var target = this.$targets[this.$targets.length - 1]; if (this.$activeTarget !== target) { this.activate(target); } return; } if (this.$activeTarget && scrollTop < this.$offsets[0] && this.$offsets[0] > 0) { this.$activeTarget = null; this.clear(); return; } for (var i = this.$offsets.length; i--;) { var isActiveTarget = this.$activeTarget !== this.$targets[i] && scrollTop >= this.$offsets[i] && (typeof this.$offsets[i + 1] === 'undefined' || scrollTop < this.$offsets[i + 1]); if (isActiveTarget) { this.activate(this.$targets[i]); } } } }, { key: 'getScroller', value: function getScroller() { if (this.$scroller) { return this.$scroller; } var scroller = this.$config.element; if (!scroller) { return null; } else if (isElement(scroller.$el)) { scroller = scroller.$el; } else if (typeof scroller === 'string') { scroller = select(scroller); } if (!scroller) { return null; } this.$scroller = scroller.tagName === 'BODY' ? window : scroller; return this.$scroller; } }, { key: 'getScrollTop', value: function getScrollTop() { var scroller = this.getScroller(); return scroller === window ? scroller.pageYOffset : scroller.scrollTop; } }, { key: 'getScrollHeight', value: function getScrollHeight() { return this.getScroller().scrollHeight || Math.max(document.body.scrollHeight, document.documentElement.scrollHeight); } }, { key: 'getOffsetHeight', value: function getOffsetHeight() { var scroller = this.getScroller(); return scroller === window ? window.innerHeight : getBCR(scroller).height; } }, { key: 'activate', value: function activate(target) { var _this5 = this; this.$activeTarget = target; this.clear(); // Grab the list of target links (<a href="{$target}">) var links = selectAll(this.$selector.split(',').map(function (selector) { return selector + '[href="' + target + '"]'; }).join(','), this.$el); links.forEach(function (link) { if (hasClass(link, ClassName.DROPDOWN_ITEM)) { // This is a dropdown item, so find the .dropdown-toggle and set it's state var dropdown = closest(Selector.DROPDOWN, link); if (dropdown) { _this5.setActiveState(select(Selector.DROPDOWN_TOGGLE, dropdown), true); } // Also set this link's state _this5.setActiveState(link, true); } else { // Set triggered link as active _this5.setActiveState(link, true); if (matches(link.parentElement, Selector.NAV_ITEMS)) { // Handle nav-link inside nav-item, and set nav-item active _this5.setActiveState(link.parentElement, true); } // Set triggered links parents as active // With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestor var el = link; while (el) { el = closest(Selector.NAV_LIST_GROUP, el); var sibling = el ? el.previousElementSibling : null; if (matches(sibling, Selector.NAV_LINKS + ', ' + Selector.LIST_ITEMS)) { _this5.setActiveState(sibling, true); } // Handle special case where nav-link is inside a nav-item if (matches(sibling, Selector.NAV_ITEMS)) { _this5.setActiveState(select(Selector.NAV_LINKS, sibling), true); // Add active state to nav-item as well _this5.setActiveState(sibling, true); } } } }); // Signal event to via $root, passing ID of activaed target and reference to array of links if (links && links.length > 0 && this.$root) { this.$root.$emit(ACTIVATE_EVENT, target, links); } } }, { key: 'clear', value: function clear() { var _this6 = this; selectAll(this.$selector + ', ' + Selector.NAV_ITEMS, this.$el).filter(function (el) { return hasClass(el, ClassName.ACTIVE); }).forEach(function (el) { return _this6.setActiveState(el, false); }); } }, { key: 'setActiveState', value: function setActiveState(el, active) { if (!el) { return; } if (active) { addClass(el, ClassName.ACTIVE); } else { removeClass(el, ClassName.ACTIVE); } } }], [{ key: 'Name', get: function get() { return NAME; } }, { key: 'Default', get: function get() { return Default; } }, { key: 'DefaultType', get: function get() { return DefaultType; } }]); return ScrollSpy; }(); export default ScrollSpy;