123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508 |
- 'use strict';
- import $ from 'jquery';
- import { onLoad, GetYoDigits } from './foundation.core.utils';
- import { MediaQuery } from './foundation.util.mediaQuery';
- import { Plugin } from './foundation.core.plugin';
- import { Triggers } from './foundation.util.triggers';
- /**
- * Sticky module.
- * @module foundation.sticky
- * @requires foundation.util.triggers
- * @requires foundation.util.mediaQuery
- */
- class Sticky extends Plugin {
- /**
- * Creates a new instance of a sticky thing.
- * @class
- * @name Sticky
- * @param {jQuery} element - jQuery object to make sticky.
- * @param {Object} options - options object passed when creating the element programmatically.
- */
- _setup(element, options) {
- this.$element = element;
- this.options = $.extend({}, Sticky.defaults, this.$element.data(), options);
- this.className = 'Sticky'; // ie9 back compat
- // Triggers init is idempotent, just need to make sure it is initialized
- Triggers.init($);
- this._init();
- }
- /**
- * Initializes the sticky element by adding classes, getting/setting dimensions, breakpoints and attributes
- * @function
- * @private
- */
- _init() {
- MediaQuery._init();
- var $parent = this.$element.parent('[data-sticky-container]'),
- id = this.$element[0].id || GetYoDigits(6, 'sticky'),
- _this = this;
- if($parent.length){
- this.$container = $parent;
- } else {
- this.wasWrapped = true;
- this.$element.wrap(this.options.container);
- this.$container = this.$element.parent();
- }
- this.$container.addClass(this.options.containerClass);
- this.$element.addClass(this.options.stickyClass).attr({ 'data-resize': id, 'data-mutate': id });
- if (this.options.anchor !== '') {
- $('#' + _this.options.anchor).attr({ 'data-mutate': id });
- }
- this.scrollCount = this.options.checkEvery;
- this.isStuck = false;
- this.onLoadListener = onLoad($(window), function () {
- //We calculate the container height to have correct values for anchor points offset calculation.
- _this.containerHeight = _this.$element.css("display") == "none" ? 0 : _this.$element[0].getBoundingClientRect().height;
- _this.$container.css('height', _this.containerHeight);
- _this.elemHeight = _this.containerHeight;
- if (_this.options.anchor !== '') {
- _this.$anchor = $('#' + _this.options.anchor);
- } else {
- _this._parsePoints();
- }
- _this._setSizes(function () {
- var scroll = window.pageYOffset;
- _this._calc(false, scroll);
- //Unstick the element will ensure that proper classes are set.
- if (!_this.isStuck) {
- _this._removeSticky((scroll >= _this.topPoint) ? false : true);
- }
- });
- _this._events(id.split('-').reverse().join('-'));
- });
- }
- /**
- * If using multiple elements as anchors, calculates the top and bottom pixel values the sticky thing should stick and unstick on.
- * @function
- * @private
- */
- _parsePoints() {
- var top = this.options.topAnchor == "" ? 1 : this.options.topAnchor,
- btm = this.options.btmAnchor== "" ? document.documentElement.scrollHeight : this.options.btmAnchor,
- pts = [top, btm],
- breaks = {};
- for (var i = 0, len = pts.length; i < len && pts[i]; i++) {
- var pt;
- if (typeof pts[i] === 'number') {
- pt = pts[i];
- } else {
- var place = pts[i].split(':'),
- anchor = $(`#${place[0]}`);
- pt = anchor.offset().top;
- if (place[1] && place[1].toLowerCase() === 'bottom') {
- pt += anchor[0].getBoundingClientRect().height;
- }
- }
- breaks[i] = pt;
- }
- this.points = breaks;
- return;
- }
- /**
- * Adds event handlers for the scrolling element.
- * @private
- * @param {String} id - pseudo-random id for unique scroll event listener.
- */
- _events(id) {
- var _this = this,
- scrollListener = this.scrollListener = `scroll.zf.${id}`;
- if (this.isOn) { return; }
- if (this.canStick) {
- this.isOn = true;
- $(window).off(scrollListener)
- .on(scrollListener, function(e) {
- if (_this.scrollCount === 0) {
- _this.scrollCount = _this.options.checkEvery;
- _this._setSizes(function() {
- _this._calc(false, window.pageYOffset);
- });
- } else {
- _this.scrollCount--;
- _this._calc(false, window.pageYOffset);
- }
- });
- }
- this.$element.off('resizeme.zf.trigger')
- .on('resizeme.zf.trigger', function(e, el) {
- _this._eventsHandler(id);
- });
- this.$element.on('mutateme.zf.trigger', function (e, el) {
- _this._eventsHandler(id);
- });
- if(this.$anchor) {
- this.$anchor.on('mutateme.zf.trigger', function (e, el) {
- _this._eventsHandler(id);
- });
- }
- }
- /**
- * Handler for events.
- * @private
- * @param {String} id - pseudo-random id for unique scroll event listener.
- */
- _eventsHandler(id) {
- var _this = this,
- scrollListener = this.scrollListener = `scroll.zf.${id}`;
- _this._setSizes(function() {
- _this._calc(false);
- if (_this.canStick) {
- if (!_this.isOn) {
- _this._events(id);
- }
- } else if (_this.isOn) {
- _this._pauseListeners(scrollListener);
- }
- });
- }
- /**
- * Removes event handlers for scroll and change events on anchor.
- * @fires Sticky#pause
- * @param {String} scrollListener - unique, namespaced scroll listener attached to `window`
- */
- _pauseListeners(scrollListener) {
- this.isOn = false;
- $(window).off(scrollListener);
- /**
- * Fires when the plugin is paused due to resize event shrinking the view.
- * @event Sticky#pause
- * @private
- */
- this.$element.trigger('pause.zf.sticky');
- }
- /**
- * Called on every `scroll` event and on `_init`
- * fires functions based on booleans and cached values
- * @param {Boolean} checkSizes - true if plugin should recalculate sizes and breakpoints.
- * @param {Number} scroll - current scroll position passed from scroll event cb function. If not passed, defaults to `window.pageYOffset`.
- */
- _calc(checkSizes, scroll) {
- if (checkSizes) { this._setSizes(); }
- if (!this.canStick) {
- if (this.isStuck) {
- this._removeSticky(true);
- }
- return false;
- }
- if (!scroll) { scroll = window.pageYOffset; }
- if (scroll >= this.topPoint) {
- if (scroll <= this.bottomPoint) {
- if (!this.isStuck) {
- this._setSticky();
- }
- } else {
- if (this.isStuck) {
- this._removeSticky(false);
- }
- }
- } else {
- if (this.isStuck) {
- this._removeSticky(true);
- }
- }
- }
- /**
- * Causes the $element to become stuck.
- * Adds `position: fixed;`, and helper classes.
- * @fires Sticky#stuckto
- * @function
- * @private
- */
- _setSticky() {
- var _this = this,
- stickTo = this.options.stickTo,
- mrgn = stickTo === 'top' ? 'marginTop' : 'marginBottom',
- notStuckTo = stickTo === 'top' ? 'bottom' : 'top',
- css = {};
- css[mrgn] = `${this.options[mrgn]}em`;
- css[stickTo] = 0;
- css[notStuckTo] = 'auto';
- this.isStuck = true;
- this.$element.removeClass(`is-anchored is-at-${notStuckTo}`)
- .addClass(`is-stuck is-at-${stickTo}`)
- .css(css)
- /**
- * Fires when the $element has become `position: fixed;`
- * Namespaced to `top` or `bottom`, e.g. `sticky.zf.stuckto:top`
- * @event Sticky#stuckto
- */
- .trigger(`sticky.zf.stuckto:${stickTo}`);
- this.$element.on("transitionend webkitTransitionEnd oTransitionEnd otransitionend MSTransitionEnd", function() {
- _this._setSizes();
- });
- }
- /**
- * Causes the $element to become unstuck.
- * Removes `position: fixed;`, and helper classes.
- * Adds other helper classes.
- * @param {Boolean} isTop - tells the function if the $element should anchor to the top or bottom of its $anchor element.
- * @fires Sticky#unstuckfrom
- * @private
- */
- _removeSticky(isTop) {
- var stickTo = this.options.stickTo,
- stickToTop = stickTo === 'top',
- css = {},
- anchorPt = (this.points ? this.points[1] - this.points[0] : this.anchorHeight) - this.elemHeight,
- mrgn = stickToTop ? 'marginTop' : 'marginBottom',
- notStuckTo = stickToTop ? 'bottom' : 'top',
- topOrBottom = isTop ? 'top' : 'bottom';
- css[mrgn] = 0;
- css['bottom'] = 'auto';
- if(isTop) {
- css['top'] = 0;
- } else {
- css['top'] = anchorPt;
- }
- this.isStuck = false;
- this.$element.removeClass(`is-stuck is-at-${stickTo}`)
- .addClass(`is-anchored is-at-${topOrBottom}`)
- .css(css)
- /**
- * Fires when the $element has become anchored.
- * Namespaced to `top` or `bottom`, e.g. `sticky.zf.unstuckfrom:bottom`
- * @event Sticky#unstuckfrom
- */
- .trigger(`sticky.zf.unstuckfrom:${topOrBottom}`);
- }
- /**
- * Sets the $element and $container sizes for plugin.
- * Calls `_setBreakPoints`.
- * @param {Function} cb - optional callback function to fire on completion of `_setBreakPoints`.
- * @private
- */
- _setSizes(cb) {
- this.canStick = MediaQuery.is(this.options.stickyOn);
- if (!this.canStick) {
- if (cb && typeof cb === 'function') { cb(); }
- }
- var _this = this,
- newElemWidth = this.$container[0].getBoundingClientRect().width,
- comp = window.getComputedStyle(this.$container[0]),
- pdngl = parseInt(comp['padding-left'], 10),
- pdngr = parseInt(comp['padding-right'], 10);
- if (this.$anchor && this.$anchor.length) {
- this.anchorHeight = this.$anchor[0].getBoundingClientRect().height;
- } else {
- this._parsePoints();
- }
- this.$element.css({
- 'max-width': `${newElemWidth - pdngl - pdngr}px`
- });
- var newContainerHeight = this.$element[0].getBoundingClientRect().height || this.containerHeight;
- if (this.$element.css("display") == "none") {
- newContainerHeight = 0;
- }
- this.containerHeight = newContainerHeight;
- this.$container.css({
- height: newContainerHeight
- });
- this.elemHeight = newContainerHeight;
- if (!this.isStuck) {
- if (this.$element.hasClass('is-at-bottom')) {
- var anchorPt = (this.points ? this.points[1] - this.$container.offset().top : this.anchorHeight) - this.elemHeight;
- this.$element.css('top', anchorPt);
- }
- }
- this._setBreakPoints(newContainerHeight, function() {
- if (cb && typeof cb === 'function') { cb(); }
- });
- }
- /**
- * Sets the upper and lower breakpoints for the element to become sticky/unsticky.
- * @param {Number} elemHeight - px value for sticky.$element height, calculated by `_setSizes`.
- * @param {Function} cb - optional callback function to be called on completion.
- * @private
- */
- _setBreakPoints(elemHeight, cb) {
- if (!this.canStick) {
- if (cb && typeof cb === 'function') { cb(); }
- else { return false; }
- }
- var mTop = emCalc(this.options.marginTop),
- mBtm = emCalc(this.options.marginBottom),
- topPoint = this.points ? this.points[0] : this.$anchor.offset().top,
- bottomPoint = this.points ? this.points[1] : topPoint + this.anchorHeight,
- // topPoint = this.$anchor.offset().top || this.points[0],
- // bottomPoint = topPoint + this.anchorHeight || this.points[1],
- winHeight = window.innerHeight;
- if (this.options.stickTo === 'top') {
- topPoint -= mTop;
- bottomPoint -= (elemHeight + mTop);
- } else if (this.options.stickTo === 'bottom') {
- topPoint -= (winHeight - (elemHeight + mBtm));
- bottomPoint -= (winHeight - mBtm);
- } else {
- //this would be the stickTo: both option... tricky
- }
- this.topPoint = topPoint;
- this.bottomPoint = bottomPoint;
- if (cb && typeof cb === 'function') { cb(); }
- }
- /**
- * Destroys the current sticky element.
- * Resets the element to the top position first.
- * Removes event listeners, JS-added css properties and classes, and unwraps the $element if the JS added the $container.
- * @function
- */
- _destroy() {
- this._removeSticky(true);
- this.$element.removeClass(`${this.options.stickyClass} is-anchored is-at-top`)
- .css({
- height: '',
- top: '',
- bottom: '',
- 'max-width': ''
- })
- .off('resizeme.zf.trigger')
- .off('mutateme.zf.trigger');
- if (this.$anchor && this.$anchor.length) {
- this.$anchor.off('change.zf.sticky');
- }
- if (this.scrollListener) $(window).off(this.scrollListener)
- if (this.onLoadListener) $(window).off(this.onLoadListener)
- if (this.wasWrapped) {
- this.$element.unwrap();
- } else {
- this.$container.removeClass(this.options.containerClass)
- .css({
- height: ''
- });
- }
- }
- }
- Sticky.defaults = {
- /**
- * Customizable container template. Add your own classes for styling and sizing.
- * @option
- * @type {string}
- * @default '<div data-sticky-container></div>'
- */
- container: '<div data-sticky-container></div>',
- /**
- * Location in the view the element sticks to. Can be `'top'` or `'bottom'`.
- * @option
- * @type {string}
- * @default 'top'
- */
- stickTo: 'top',
- /**
- * If anchored to a single element, the id of that element.
- * @option
- * @type {string}
- * @default ''
- */
- anchor: '',
- /**
- * If using more than one element as anchor points, the id of the top anchor.
- * @option
- * @type {string}
- * @default ''
- */
- topAnchor: '',
- /**
- * If using more than one element as anchor points, the id of the bottom anchor.
- * @option
- * @type {string}
- * @default ''
- */
- btmAnchor: '',
- /**
- * Margin, in `em`'s to apply to the top of the element when it becomes sticky.
- * @option
- * @type {number}
- * @default 1
- */
- marginTop: 1,
- /**
- * Margin, in `em`'s to apply to the bottom of the element when it becomes sticky.
- * @option
- * @type {number}
- * @default 1
- */
- marginBottom: 1,
- /**
- * Breakpoint string that is the minimum screen size an element should become sticky.
- * @option
- * @type {string}
- * @default 'medium'
- */
- stickyOn: 'medium',
- /**
- * Class applied to sticky element, and removed on destruction. Foundation defaults to `sticky`.
- * @option
- * @type {string}
- * @default 'sticky'
- */
- stickyClass: 'sticky',
- /**
- * Class applied to sticky container. Foundation defaults to `sticky-container`.
- * @option
- * @type {string}
- * @default 'sticky-container'
- */
- containerClass: 'sticky-container',
- /**
- * Number of scroll events between the plugin's recalculating sticky points. Setting it to `0` will cause it to recalc every scroll event, setting it to `-1` will prevent recalc on scroll.
- * @option
- * @type {number}
- * @default -1
- */
- checkEvery: -1
- };
- /**
- * Helper function to calculate em values
- * @param Number {em} - number of em's to calculate into pixels
- */
- function emCalc(em) {
- return parseInt(window.getComputedStyle(document.body, null).fontSize, 10) * em;
- }
- export {Sticky};
|