Skip to content

Commit

Permalink
[scrollspy] Better options handling (#375)
Browse files Browse the repository at this point in the history
* Create README.md

* Create meta.json

* Create index.json

* [docs] Added button-toolbar

* ESLint

* [scrollspy] Better options handling

Better handling of modifiers
A few code optimizations
Removed excess usage comments

* ESLint
  • Loading branch information
tmorehouse authored and pi0 committed May 10, 2017
1 parent 3e9b860 commit d7c09fe
Showing 1 changed file with 78 additions and 123 deletions.
201 changes: 78 additions & 123 deletions lib/directives/scrollspy.js
Original file line number Diff line number Diff line change
@@ -1,90 +1,10 @@
/*
* Directive v-b-scrollspy
*
* Usage:
* Assume body is the scroll element, and use default offset of 10 pixels
* <ul v-b-scrollspy>
* <li><a href="#bar">Foo</a></li>
* <li><a href="#baz">Bar</a></li>
* </el>
*
* Assume body is the scroll element, and use offset of 20 pixels
* <ul v-b-scrollspy="20">
* <li><a href="#bar">Foo</a></li>
* <li><a href="#baz">Bar</a></li>
* </el>
*
* Element with ID #foo is the scroll element, and use default offset of 10 pixels
* <ul v-b-scrollspy:foo>
* <li><a href="#bar">Foo</a></li>
* <li><a href="#baz">Bar</a></li>
* </el>
*
* #foo is the scroll element, and use offset of 20 pixels
* <ul v-b-scrollspy:foo="20">
* <li><a href="#bar">Foo</a></li>
* <li><a href="#baz">Bar</a></li>
* </el>
*
* #foo is the scroll element, and use offset of 25 pixels
* <ul v-b-scrollspy:foo.25>
* <li><a href="#foo">Foo</a></li>
* <li><a href="#bar">Bar</a></li>
* </el>
*
* #foo is the scroll element, and use default offset of 10 pixels
* <ul v-b-scrollspy="'#foo'">
* <li><a href="#foo">Foo</a></li>
* <li><a href="#bar">Bar</a></li>
* </el>
*
* Pass object as config element can be a CSS ID, a CSS selector (i.e. body), or a node reference
* <ul v-b-scrollspy="{element: '#id', offset: 50}">
* <li><a href="#bar">Foo</a></li>
* <li><a href="#baz">Bar</a></li>
* </el>
*
* If scroll element is not present, then we assume scrolling on 'body'
* If scroll element is a CSS selector, the first found element is chosen
* if scroll element is not found, then ScrollSpy silently does nothing
*
* Config object properties:
* config = {
* element: <cssstring|elementref>,
* offset: <number>,
* method: <auto|position|offset>,
* throttle: <number>
* }
*
* element:
* Element to be monitored for swcrolling. defaults to 'body'. can be an ID (#foo), a
* css Selector (#foo div), or a reference to an element node. If a CSS string, then
* the first matching element is used. if an ID is sued it must start with '#'
* offset:
* offset befor triggering active state, number of pixels. defaults to 10
* method:
* method of calculating target offets.
* 'auto' will choose 'offset' if scroll element is 'body', else 'position'
* 'position' will calculate target offsets relative to the scroll contaner.
* 'offset' will calulate the target offsets relative to the top of the window/viewport
* Defaults to 'auto'
* throttle:
* timeout for resize events to stop firing before recalculating offsets.
* defaults to 200ms
*
* if args/modifiers and a value (object or number) is passed, the value takes presidence over
* the arg and modifiers
*
* Events:
* Whenever a target is activted, the event 'scrollspy::activate' is emitted on $root with the
* targets HREF (ID) as the argument
*/
const inBrowser = typeof window !== 'undefined';
const isServer = !inBrowser;

/*
* Pollyfill for Element.closest() for IE :(
* https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
*/
const inBrowser = typeof window !== 'undefined';

if (inBrowser && window.Element && !Element.prototype.closest) {
Element.prototype.closest = function (s) {
Expand Down Expand Up @@ -149,24 +69,57 @@ const OffsetMethod = {
POSITION: 'position'
};

const isServer = typeof window === 'undefined';
/*
* DOM Utility Methods
*/

function isElement(obj) {
return obj.nodeType;
}

// Wrapper for Element.closest to emulate jQuery's closest (sorta)
function closest(element, selector) {
const el = element.closest(selector);
return el === element ? null : el;
}

// Query Selector All wrapper
function $QSA(selector, element) {
if (!element) {
element = document;
}
if (!isElement(element)) {
return [];
}
return Array.prototype.slice.call(element.querySelectorAll(selector));
}

// Query Selector wrapper
function $QS(selector, element) {
if (!element) {
element = document;
}
if (!isElement(element)) {
return null;
}
return element.querySelector(selector) || null;
}

/*
* Utility Methods
*/

// Get Vue VM from element
function getVm(el) {
return el ? el.__vue__ : null;
}

// Better var type detection
function toType(obj) {
return ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
}

function isElement(obj) {
return obj.nodeType;
}

// Check config properties for expected types
function typeCheckConfig(componentName, config, configTypes) {
for (const property in configTypes) {
if (Object.prototype.hasOwnProperty.call(configTypes, property)) {
Expand All @@ -176,27 +129,21 @@ function typeCheckConfig(componentName, config, configTypes) {

if (!new RegExp(expectedTypes).test(valueType)) {
console.error(
NAME + ': Option "' + property + '" provided type "' +
componentName + ': Option "' + property + '" provided type "' +
valueType + '" but expected type "' + expectedTypes + '"'
);
}
}
}
}

// Wrapper for Element.closest to emulate jQuery's closest (sorta)
function closest(element, selector) {
const el = element.closest(selector);
return el === element ? null : el;
}

/*
* ScrollSpy Class
*/

function ScrollSpy(el, binding) {
// The element that contains the nav-links et al
this._element = el;
this._$el = el;
// The selectors to find the nav-links
this._selector = [
Selector.NAV_LINKS,
Expand All @@ -208,12 +155,13 @@ function ScrollSpy(el, binding) {
// Target HREF IDs and their offsets
this._offsets = [];
this._targets = [];
// The currently active target (as an HREF id
// The currently active target (as an HREF id)
this._activeTarget = null;
// Curent scroll height (for detecting document height changes)
this._scrollHeight = 0;
// Reference to the $root VM so we can spew events
this._$root = null;
// Reference to our throttled resize hanlder
// Reference to our throttled resize timeout
this._resizeTimeout = null;

// Process bindings/config
Expand All @@ -226,21 +174,24 @@ function ScrollSpy(el, binding) {

// Update config
ScrollSpy.prototype.updateConfig = function (binding) {
// If Argument, assume element ID
if (binding.arg) {
// Element ID specified as arg. We must pre-pend #
this._config.element = '#' + binding.arg;
}
if (binding.modifiers.length > 0) {
for (let i = 0; i < binding.modifiers.length - 1; i++) {
if (/^\d+$/.test(binding.modifiers[i])) {
// Assume offest value
this._config.offset = parseInt(binding.modifiers[0], 10);
} else if (/^(position|offset)$/.test(binding.modifiers[i])) {
// Assume offset method
this._config.method = binding.modifiers[i];
}

// Process modifiers
Object.keys(binding.modifiers).forEach(val => {
if (/^\d+$/.test(val)) {
// Offest value
this._config.offset = parseInt(val, 10);
} else if (/^(auto|position|offset)$/.test(val)) {
// Offset method
this._config.method = val;
}
}
});

// Process value
if (typeof binding.value === 'string') {
// Value is a CSS ID or selector
this._config.element = binding.value;
Expand All @@ -249,12 +200,16 @@ ScrollSpy.prototype.updateConfig = function (binding) {
this._config.offset = Math.round(binding.value);
} else if (typeof binding.value === 'object') {
// Value is config object
this._config = Object.assign({}, this._config, binding.value);
Object.keys(binding.value).filter(k => Boolean(DefaultType[k])).forEach(k => {
this._config[k] = binding.value[k];
});
}

// Check the config and log error to console. Unknown options are ignored
typeCheckConfig(NAME, this._config, DefaultType);

const vm = getVm(this._element);
// Get Vue instance from element
const vm = getVm(this._$el);
if (vm && vm.$root) {
this._$root = vm.$root;
}
Expand Down Expand Up @@ -308,12 +263,10 @@ ScrollSpy.prototype.refresh = function () {
this._scrollHeight = this._getScrollHeight();

// Find all nav link/dropdown/list-item links in our element
const navs = Array.prototype.slice.call(this._element.querySelectorAll(this._selector));

navs.map(el => {
$QSA(this._selector, this._$el).map(el => {
const href = el.getAttribute('href');
if (href && href.charAt(0) === '#' && href !== '#' && href.indexOf('#/') === -1) {
const target = scroller.querySelector(href);
const target = $QS(href, scroller);
if (!target) {
return null;
}
Expand Down Expand Up @@ -388,7 +341,7 @@ ScrollSpy.prototype.dispose = function () {
// Garbage collection
clearTimeout(this._resizeTimeout);
this._resizeTimeout = null;
this._element = null;
this._$el = null;
this._config = null;
this._selector = null;
this._offsets = null;
Expand Down Expand Up @@ -441,11 +394,12 @@ ScrollSpy.prototype._getScroller = function () {
return document.body;
}
// Otherwise assume CSS selector
return document.querySelector(scroller);
return $QS(scroller);
}
return null;
};

// Return the scroller top position
ScrollSpy.prototype._getScrollTop = function () {
const scroller = this._getScroller();
if (!scroller) {
Expand All @@ -454,6 +408,7 @@ ScrollSpy.prototype._getScrollTop = function () {
return scroller.tagName === 'BODY' ? window.pageYOffset : scroller.scrollTop;
};

// Return the scroller height
ScrollSpy.prototype._getScrollHeight = function () {
const scroller = this._getScroller();
if (!scroller) {
Expand All @@ -464,6 +419,7 @@ ScrollSpy.prototype._getScrollHeight = function () {
scroller.scrollHeight;
};

// Return the scroller offset top position
ScrollSpy.prototype._getOffsetHeight = function () {
const scroller = this._getScroller();
if (!scroller) {
Expand All @@ -472,6 +428,7 @@ ScrollSpy.prototype._getOffsetHeight = function () {
return scroller.tagName === 'BODY' ? window.innerHeight : scroller.getBoundingClientRect().height;
};

// Activate the scrolled in target nav-link
ScrollSpy.prototype._activate = function (target) {
this._activeTarget = target;
this._clear();
Expand All @@ -481,14 +438,14 @@ ScrollSpy.prototype._activate = function (target) {
return selector + '[href="' + target + '"]';
});

const links = Array.prototype.slice.call(this._element.querySelectorAll(queries.join(',')));
const links = $QSA(queries.join(','), this._$el);

links.forEach(link => {
if (link.classList.contains(ClassName.DROPDOWN_ITEM)) {
// This is a dropdown item, so find the .dropdown-toggle and set it's state
const dropdown = closest(link, Selector.DROPDOWN);
if (dropdown) {
const toggle = dropdown.querySelector(Selector.DROPDOWN_TOGGLE);
const toggle = $QS(Selector.DROPDOWN_TOGGLE, dropdown);
if (toggle) {
this._setActiveState(toggle, true);
}
Expand All @@ -506,16 +463,14 @@ ScrollSpy.prototype._activate = function (target) {
});

// Signal event to root, passing ID of target
if (links && links.length > 0) {
if (this._$root && this._$root.$emit) {
this._$root.$emit(EVENT, target);
}
if (links && links.length > 0 && this._$root && this._$root.$emit) {
this._$root.$emit(EVENT, target);
}
};

// Clear the 'active' targets in our nav component
ScrollSpy.prototype._clear = function () {
const els = Array.prototype.slice.call(document.querySelectorAll(this._selector));
els.filter(el => {
$QSA(this._selector, this._$el).filter(el => {
if (el.classList.contains(ClassName.ACTIVE)) {
const href = el.getAttribute('href');
if (href.charAt(0) !== '#' || href.indexOf('#/') === 0) {
Expand Down

0 comments on commit d7c09fe

Please sign in to comment.