From: Chris Oyler Date: Thu, 28 Jan 2016 17:22:46 +0000 (-0800) Subject: updating parts of reveal modal to better be able to position itself on open and resiz... X-Git-Tag: v6.2.0-rc.1~35^2~12 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=a0bb3d109c7c20edd4d4d5edd2f792e3022e6324;p=thirdparty%2Ffoundation%2Ffoundation-sites.git updating parts of reveal modal to better be able to position itself on open and resize events. --- diff --git a/js/foundation.new-reveal.js b/js/foundation.new-reveal.js new file mode 100644 index 000000000..ecaa934a6 --- /dev/null +++ b/js/foundation.new-reveal.js @@ -0,0 +1,522 @@ +/** + * Reveal module. + * @module foundation.reveal + * @requires foundation.util.keyboard + * @requires foundation.util.box + * @requires foundation.util.triggers + * @requires foundation.util.mediaQuery + * @requires foundation.util.motion if using animations + */ +!function(Foundation, $) { + 'use strict'; + + /** + * Creates a new instance of Reveal. + * @class + * @param {jQuery} element - jQuery object to use for the modal. + * @param {Object} options - optional parameters. + */ + + function Reveal(element, options) { + this.$element = element; + this.options = $.extend({}, Reveal.defaults, this.$element.data(), options); + this._init(); + + Foundation.registerPlugin(this, 'Reveal'); + // Foundation.Keyboard.register('Reveal', { + // 'ENTER': 'open', + // 'SPACE': 'open', + // 'ESCAPE': 'close', + // 'TAB': 'tab_forward', + // 'SHIFT_TAB': 'tab_backward' + // }); + } + + Reveal.defaults = { + /** + * Motion-UI class to use for animated elements. If none used, defaults to simple show/hide. + * @option + * @example 'slide-in-left' + */ + animationIn: '', + /** + * Motion-UI class to use for animated elements. If none used, defaults to simple show/hide. + * @option + * @example 'slide-out-right' + */ + animationOut: '', + /** + * Time, in ms, to delay the opening of a modal after a click if no animation used. + * @option + * @example 10 + */ + showDelay: 0, + /** + * Time, in ms, to delay the closing of a modal after a click if no animation used. + * @option + * @example 10 + */ + hideDelay: 0, + /** + * Allows a click on the body/overlay to close the modal. + * @option + * @example true + */ + closeOnClick: true, + /** + * Allows the modal to close if the user presses the `ESCAPE` key. + * @option + * @example true + */ + closeOnEsc: true, + /** + * If true, allows multiple modals to be displayed at once. + * @option + * @example false + */ + multipleOpened: false, + /** + * Distance, in pixels, the modal should push down from the top of the screen. + * @option + * @example 100 + */ + vOffset: 100, + /** + * Distance, in pixels, the modal should push in from the side of the screen. + * @option + * @example 0 + */ + hOffset: 0, + /** + * Allows the modal to be fullscreen, completely blocking out the rest of the view. JS checks for this as well. + * @option + * @example false + */ + fullScreen: false, + /** + * Percentage of screen height the modal should push up from the bottom of the view. + * @option + * @example 10 + */ + btmOffsetPct: 10, + /** + * Allows the modal to generate an overlay div, which will cover the view when modal opens. + * @option + * @example true + */ + overlay: true, + /** + * Allows the modal to remove and reinject markup on close. Should be true if using video elements w/o using provider's api, otherwise, videos will continue to play in the background. + * @option + * @example false + */ + resetOnClose: false, + /** + * Allows the modal to alter the url on open/close, and allows the use of the `back` button to close modals. ALSO, allows a modal to auto-maniacally open on page load IF the hash === the modal's user-set id. + * @option + * @example false + */ + deepLink: false + }; + + /** + * Initializes the modal by adding the overlay and close buttons, (if selected). + * @private + */ + Reveal.prototype._init = function(){ + this.id = this.$element.attr('id'); + this.isActive = false; + + this.$anchor = $('[data-open="' + this.id + '"]').length ? $('[data-open="' + this.id + '"]') : $('[data-toggle="' + this.id + '"]'); + + if(this.$anchor.length){ + var anchorId = this.$anchor[0].id || Foundation.GetYoDigits(6, 'reveal'); + + this.$anchor.attr({ + 'aria-controls': this.id, + 'id': anchorId, + 'aria-haspopup': true, + 'tabindex': 0 + }); + this.$element.attr({'aria-labelledby': anchorId}); + } + + // this.options.fullScreen = this.$element.hasClass('full'); + if(this.options.fullScreen || this.$element.hasClass('full')){ + this.options.fullScreen = true; + this.options.overlay = false; + } + if(this.options.overlay && !this.$overlay){ + this.$overlay = this._makeOverlay(this.id); + } + + this.$element.attr({ + 'role': 'dialog', + 'aria-hidden': true, + 'data-yeti-box': this.id, + 'data-resize': this.id + }); + + this._events(); + if(this.options.deepLink && window.location.hash === ( '#' + this.id)){ + $(window).one('load.zf.reveal', this.open.bind(this)); + } + }; + + /** + * Creates an overlay div to display behind the modal. + * @private + */ + Reveal.prototype._makeOverlay = function(id){ + var $overlay = $('
') + .addClass('reveal-overlay') + .attr({'tabindex': -1, 'aria-hidden': true}) + .appendTo('body'); + if(this.options.closeOnClick){ + $overlay.attr({ + 'data-close': id + }); + } + return $overlay; + }; + + /** + * Adds event handlers for the modal. + * @private + */ + Reveal.prototype._events = function(){ + var _this = this; + + this.$element.on({ + 'open.zf.trigger': this.open.bind(this), + 'close.zf.trigger': this.close.bind(this), + 'toggle.zf.trigger': this.toggle.bind(this), + 'resizeme.zf.trigger': function(){ + if(_this.$element.is(':visible')){ + _this._setPosition(function(){}); + } + } + }); + + if(this.$anchor.length){ + this.$anchor.on('keydown.zf.reveal', function(e){ + if(e.which === 13 || e.which === 32){ + e.stopPropagation(); + e.preventDefault(); + _this.open(); + } + }); + } + + + if(this.options.closeOnClick && this.options.overlay){ + this.$overlay.off('.zf.reveal').on('click.zf.reveal', this.close.bind(this)); + } + if(this.options.deepLink){ + $(window).on('popstate.zf.reveal:' + this.id, this._handleState.bind(this)); + } + }; + /** + * Handles modal methods on back/forward button clicks or any other event that triggers popstate. + * @private + */ + Reveal.prototype._handleState = function(e){ + if(window.location.hash === ( '#' + this.id) && !this.isActive){ this.open(); } + else{ this.close(); } + }; + /** + * Sets the position of the modal before opening + * @param {Function} cb - a callback function to execute when positioning is complete. + * @private + */ + Reveal.prototype._setPosition = function(cb){ + console.log('calling'); + var winWidth = window.innerWidth, + parentOffset = this.$element.offsetParent().offset(), + x = Math.round((winWidth - this.$element[0].getBoundingClientRect().width) / 2 - (parentOffset.left > 0 ? parentOffset.left : 0)), + y = Math.round(window.pageYOffset - (parentOffset.top > 0 ? parentOffset.top : 0) + this.options.vOffset); + + this.$element.css('transform', 'translate(' + x + 'px, ' + y + 'px)'); + if(cb) cb(); + // var eleDims = Foundation.Box.GetDimensions(this.$element); + // var elePos = this.options.fullScreen ? 'reveal full' : (eleDims.height >= (0.5 * eleDims.windowDims.height)) ? 'reveal' : 'center'; + // + // if(elePos === 'reveal full'){ + // //set to full height/width + // this.$element + // .offset(Foundation.Box.GetOffsets(this.$element, null, elePos, this.options.vOffset)) + // .css({ + // 'height': eleDims.windowDims.height, + // 'width': eleDims.windowDims.width + // }); + // }else if(!Foundation.MediaQuery.atLeast('medium') || !Foundation.Box.ImNotTouchingYou(this.$element, null, true, false)){ + // //if smaller than medium, resize to 100% width minus any custom L/R margin + // this.$element + // .css({ + // 'width': eleDims.windowDims.width - (this.options.hOffset * 2) + // }) + // .offset(Foundation.Box.GetOffsets(this.$element, null, 'center', this.options.vOffset, this.options.hOffset)); + // //flag a boolean so we can reset the size after the element is closed. + // this.changedSize = true; + // }else{ + // this.$element + // .css({ + // 'max-height': eleDims.windowDims.height - (this.options.vOffset * (this.options.btmOffsetPct / 100 + 1)), + // 'width': '' + // }) + // .offset(Foundation.Box.GetOffsets(this.$element, null, elePos, this.options.vOffset)); + // //the max height based on a percentage of vertical offset plus vertical offset + // } + // + // cb(); + }; + + /** + * Opens the modal controlled by `this.$anchor`, and closes all others by default. + * @function + * @fires Reveal#closeme + * @fires Reveal#open + */ + Reveal.prototype.open = function(){ + if(this.options.deepLink){ + var hash = '#' + this.id; + + if(window.history.pushState){ + window.history.pushState(null, null, hash); + }else{ + window.location.hash = hash; + } + } + + var _this = this; + this.isActive = true; + //make element invisible, but remove display: none so we can get size and positioning + this.$element + .css({'visibility': 'hidden'}) + .show() + // .scrollTop(0); + + this._setPosition(function(){ + _this.$element.hide() + .css({'visibility': ''}); + if(!_this.options.multipleOpened){ + /** + * Fires immediately before the modal opens. + * Closes any other modals that are currently open + * @event Reveal#closeme + */ + _this.$element.trigger('closeme.zf.reveal', _this.id); + } + if(_this.options.animationIn){ + if(_this.options.overlay){ + Foundation.Motion.animateIn(_this.$overlay, 'fade-in', function(){ + Foundation.Motion.animateIn(_this.$element, _this.options.animationIn, function(){ + _this.focusableElements = Foundation.Keyboard.findFocusable(_this.$element); + }); + }); + }else{ + Foundation.Motion.animateIn(_this.$element, _this.options.animationIn, function(){ + _this.focusableElements = Foundation.Keyboard.findFocusable(_this.$element); + }); + } + }else{ + if(_this.options.overlay){ + _this.$overlay.show(0, function(){ + _this.$element.show(_this.options.showDelay, function(){ + }); + }); + }else{ + _this.$element.show(_this.options.showDelay, function(){ + }); + } + } + }); + + + // handle accessibility + this.$element.attr({'aria-hidden': false}).attr('tabindex', -1).focus() + /** + * Fires when the modal has successfully opened. + * @event Reveal#open + */ + .trigger('open.zf.reveal'); + + $('body').addClass('is-reveal-open') + .attr({'aria-hidden': (this.options.overlay || this.options.fullScreen) ? true : false}); + setTimeout(function(){ + _this._extraHandlers(); + }, 0); + }; + + /** + * Adds extra event handlers for the body and window if necessary. + * @private + */ + Reveal.prototype._extraHandlers = function(){ + var _this = this; + this.focusableElements = Foundation.Keyboard.findFocusable(this.$element); + + if(!this.options.overlay && this.options.closeOnClick && !this.options.fullScreen){ + $('body').on('click.zf.reveal', function(e){ + if(e.target === _this.$element[0] || $.contains(_this.$element[0], e.target)){ return; } + _this.close(); + }); + } + if(this.options.closeOnEsc){ + $(window).on('keydown.zf.reveal', function(e){ + Foundation.Keyboard.handleKey(e, 'Reveal', { + close: function() { + if (_this.options.closeOnEsc) { + _this.close(); + _this.$anchor.focus(); + } + } + }); + if (_this.focusableElements.length === 0) { // no focusable elements inside the modal at all, prevent tabbing in general + e.preventDefault(); + } + }); + } + + // lock focus within modal while tabbing + this.$element.on('keydown.zf.reveal', function(e) { + var $target = $(this); + // handle keyboard event with keyboard util + Foundation.Keyboard.handleKey(e, 'Reveal', { + tab_forward: function() { + if (_this.$element.find(':focus').is(_this.focusableElements.eq(-1))) { // left modal downwards, setting focus to first element + _this.focusableElements.eq(0).focus(); + e.preventDefault(); + } + }, + tab_backward: function() { + if (_this.$element.find(':focus').is(_this.focusableElements.eq(0)) || _this.$element.is(':focus')) { // left modal upwards, setting focus to last element + _this.focusableElements.eq(-1).focus(); + e.preventDefault(); + } + }, + open: function() { + if (_this.$element.find(':focus').is(_this.$element.find('[data-close]'))) { + setTimeout(function() { // set focus back to anchor if close button has been activated + _this.$anchor.focus(); + }, 1); + } else if ($target.is(_this.focusableElements)) { // dont't trigger if acual element has focus (i.e. inputs, links, ...) + _this.open(); + } + }, + close: function() { + if (_this.options.closeOnEsc) { + _this.close(); + _this.$anchor.focus(); + } + } + }); + }); + + }; + + /** + * Closes the modal. + * @function + * @fires Reveal#closed + */ + Reveal.prototype.close = function(){ + if(!this.isActive || !this.$element.is(':visible')){ + return false; + } + var _this = this; + + if(this.options.animationOut){ + Foundation.Motion.animateOut(this.$element, this.options.animationOut, function(){ + if(_this.options.overlay){ + Foundation.Motion.animateOut(_this.$overlay, 'fade-out', finishUp); + }else{ finishUp(); } + }); + }else{ + this.$element.hide(_this.options.hideDelay, function(){ + if(_this.options.overlay){ + _this.$overlay.hide(0, finishUp); + }else{ finishUp(); } + }); + } + //conditionals to remove extra event listeners added on open + if(this.options.closeOnEsc){ + $(window).off('keydown.zf.reveal'); + } + if(!this.options.overlay && this.options.closeOnClick){ + $('body').off('click.zf.reveal'); + } + this.$element.off('keydown.zf.reveal'); + function finishUp(){ + //if the modal changed size, reset it + if(_this.changedSize){ + _this.$element.css({ + 'height': '', + 'width': '' + }); + } + $('body').removeClass('is-reveal-open').attr({'aria-hidden': false, 'tabindex': ''}); + _this.$element.attr({'aria-hidden': true}) + /** + * Fires when the modal is done closing. + * @event Reveal#closed + */ + .trigger('closed.zf.reveal'); + } + + + /** + * Resets the modal content + * This prevents a running video to keep going in the background + */ + // if(this.options.resetOnClose) { + // this.$element.html(this.$element.html()); + // } + + this.isActive = false; + if(_this.options.deepLink){ + if(window.history.replaceState){ + window.history.replaceState("", document.title, window.location.pathname); + }else{ + window.location.hash = ''; + } + } + }; + /** + * Toggles the open/closed state of a modal. + * @function + */ + Reveal.prototype.toggle = function(){ + if(this.isActive){ + this.close(); + }else{ + this.open(); + } + }; + + /** + * Destroys an instance of a modal. + * @function + */ + Reveal.prototype.destroy = function() { + if(this.options.overlay){ + this.$overlay.hide().off().remove(); + } + this.$element.hide().off(); + this.$anchor.off('.zf'); + $(window).off('.zf.reveal:' + this.id); + + Foundation.unregisterPlugin(this); + }; + + Foundation.plugin(Reveal, 'Reveal'); + + // Exports for AMD/Browserify + if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') + module.exports = Reveal; + if (typeof define === 'function') + define(['foundation'], function() { + return Reveal; + }); + +}(Foundation, jQuery); diff --git a/scss/components/_reveal.scss b/scss/components/_reveal.scss index ffa382234..5c52ec746 100644 --- a/scss/components/_reveal.scss +++ b/scss/components/_reveal.scss @@ -60,10 +60,12 @@ $reveal-overlay-background: rgba($black, 0.45) !default; @mixin reveal-modal-base { @include disable-mouse-outline; display: none; + top: 0; + left: 0; z-index: $reveal-zindex + 1; padding: $reveal-padding; border: $reveal-border; - margin: $reveal-offset auto 0; + // margin: $reveal-offset auto 0; background-color: $reveal-background; border-radius: $reveal-radius;