123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630 |
- 'use strict';
- import $ from 'jquery';
- import { onLoad } from './foundation.core.utils';
- import { Keyboard } from './foundation.util.keyboard';
- import { MediaQuery } from './foundation.util.mediaQuery';
- import { Motion } from './foundation.util.motion';
- import { Plugin } from './foundation.core.plugin';
- import { Triggers } from './foundation.util.triggers';
- import { Touch } from './foundation.util.touch'
- /**
- * Reveal module.
- * @module foundation.reveal
- * @requires foundation.util.keyboard
- * @requires foundation.util.triggers
- * @requires foundation.util.mediaQuery
- * @requires foundation.util.motion if using animations
- */
- class Reveal extends Plugin {
- /**
- * Creates a new instance of Reveal.
- * @class
- * @name Reveal
- * @param {jQuery} element - jQuery object to use for the modal.
- * @param {Object} options - optional parameters.
- */
- _setup(element, options) {
- this.$element = element;
- this.options = $.extend({}, Reveal.defaults, this.$element.data(), options);
- this.className = 'Reveal'; // ie9 back compat
- this._init();
- // Triggers init is idempotent, just need to make sure it is initialized
- Triggers.init($);
- Keyboard.register('Reveal', {
- 'ESCAPE': 'close',
- });
- }
- /**
- * Initializes the modal by adding the overlay and close buttons, (if selected).
- * @private
- */
- _init() {
- MediaQuery._init();
- this.id = this.$element.attr('id');
- this.isActive = false;
- this.cached = {mq: MediaQuery.current};
- this.$anchor = $(`[data-open="${this.id}"]`).length ? $(`[data-open="${this.id}"]`) : $(`[data-toggle="${this.id}"]`);
- this.$anchor.attr({
- 'aria-controls': this.id,
- 'aria-haspopup': true,
- 'tabindex': 0
- });
- 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
- });
- if(this.$overlay) {
- this.$element.detach().appendTo(this.$overlay);
- } else {
- this.$element.detach().appendTo($(this.options.appendTo));
- this.$element.addClass('without-overlay');
- }
- this._events();
- if (this.options.deepLink && window.location.hash === ( `#${this.id}`)) {
- this.onLoadListener = onLoad($(window), () => this.open());
- }
- }
- /**
- * Creates an overlay div to display behind the modal.
- * @private
- */
- _makeOverlay() {
- var additionalOverlayClasses = '';
- if (this.options.additionalOverlayClasses) {
- additionalOverlayClasses = ' ' + this.options.additionalOverlayClasses;
- }
- return $('<div></div>')
- .addClass('reveal-overlay' + additionalOverlayClasses)
- .appendTo(this.options.appendTo);
- }
- /**
- * Updates position of modal
- * TODO: Figure out if we actually need to cache these values or if it doesn't matter
- * @private
- */
- _updatePosition() {
- var width = this.$element.outerWidth();
- var outerWidth = $(window).width();
- var height = this.$element.outerHeight();
- var outerHeight = $(window).height();
- var left, top = null;
- if (this.options.hOffset === 'auto') {
- left = parseInt((outerWidth - width) / 2, 10);
- } else {
- left = parseInt(this.options.hOffset, 10);
- }
- if (this.options.vOffset === 'auto') {
- if (height > outerHeight) {
- top = parseInt(Math.min(100, outerHeight / 10), 10);
- } else {
- top = parseInt((outerHeight - height) / 4, 10);
- }
- } else if (this.options.vOffset !== null) {
- top = parseInt(this.options.vOffset, 10);
- }
- if (top !== null) {
- this.$element.css({top: top + 'px'});
- }
- // only worry about left if we don't have an overlay or we have a horizontal offset,
- // otherwise we're perfectly in the middle
- if (!this.$overlay || (this.options.hOffset !== 'auto')) {
- this.$element.css({left: left + 'px'});
- this.$element.css({margin: '0px'});
- }
- }
- /**
- * Adds event handlers for the modal.
- * @private
- */
- _events() {
- var _this = this;
- this.$element.on({
- 'open.zf.trigger': this.open.bind(this),
- 'close.zf.trigger': (event, $element) => {
- if ((event.target === _this.$element[0]) ||
- ($(event.target).parents('[data-closable]')[0] === $element)) { // only close reveal when it's explicitly called
- return this.close.apply(this);
- }
- },
- 'toggle.zf.trigger': this.toggle.bind(this),
- 'resizeme.zf.trigger': function() {
- _this._updatePosition();
- }
- });
- if (this.options.closeOnClick && this.options.overlay) {
- this.$overlay.off('.zf.reveal').on('click.zf.reveal', function(e) {
- if (e.target === _this.$element[0] ||
- $.contains(_this.$element[0], e.target) ||
- !$.contains(document, e.target)) {
- return;
- }
- _this.close();
- });
- }
- if (this.options.deepLink) {
- $(window).on(`hashchange.zf.reveal:${this.id}`, this._handleState.bind(this));
- }
- }
- /**
- * Handles modal methods on back/forward button clicks or any other event that triggers hashchange.
- * @private
- */
- _handleState(e) {
- if(window.location.hash === ( '#' + this.id) && !this.isActive){ this.open(); }
- else{ this.close(); }
- }
- /**
- * Disables the scroll when Reveal is shown to prevent the background from shifting
- * @param {number} scrollTop - Scroll to visually apply, window current scroll by default
- */
- _disableScroll(scrollTop) {
- scrollTop = scrollTop || $(window).scrollTop();
- if ($(document).height() > $(window).height()) {
- $("html")
- .css("top", -scrollTop);
- }
- }
- /**
- * Reenables the scroll when Reveal closes
- * @param {number} scrollTop - Scroll to restore, html "top" property by default (as set by `_disableScroll`)
- */
- _enableScroll(scrollTop) {
- scrollTop = scrollTop || parseInt($("html").css("top"));
- if ($(document).height() > $(window).height()) {
- $("html")
- .css("top", "");
- $(window).scrollTop(-scrollTop);
- }
- }
- /**
- * Opens the modal controlled by `this.$anchor`, and closes all others by default.
- * @function
- * @fires Reveal#closeme
- * @fires Reveal#open
- */
- open() {
- // either update or replace browser history
- const hash = `#${this.id}`;
- if (this.options.deepLink && window.location.hash !== hash) {
- if (window.history.pushState) {
- if (this.options.updateHistory) {
- window.history.pushState({}, '', hash);
- } else {
- window.history.replaceState({}, '', hash);
- }
- } else {
- window.location.hash = hash;
- }
- }
- // Remember anchor that opened it to set focus back later, have general anchors as fallback
- this.$activeAnchor = $(document.activeElement).is(this.$anchor) ? $(document.activeElement) : this.$anchor;
- this.isActive = true;
- // Make elements invisible, but remove display: none so we can get size and positioning
- this.$element
- .css({ 'visibility': 'hidden' })
- .show()
- .scrollTop(0);
- if (this.options.overlay) {
- this.$overlay.css({'visibility': 'hidden'}).show();
- }
- this._updatePosition();
- this.$element
- .hide()
- .css({ 'visibility': '' });
- if(this.$overlay) {
- this.$overlay.css({'visibility': ''}).hide();
- if(this.$element.hasClass('fast')) {
- this.$overlay.addClass('fast');
- } else if (this.$element.hasClass('slow')) {
- this.$overlay.addClass('slow');
- }
- }
- 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);
- }
- this._disableScroll();
- var _this = this;
- // Motion UI method of reveal
- if (this.options.animationIn) {
- function afterAnimation(){
- _this.$element
- .attr({
- 'aria-hidden': false,
- 'tabindex': -1
- })
- .focus();
- _this._addGlobalClasses();
- Keyboard.trapFocus(_this.$element);
- }
- if (this.options.overlay) {
- Motion.animateIn(this.$overlay, 'fade-in');
- }
- Motion.animateIn(this.$element, this.options.animationIn, () => {
- if(this.$element) { // protect against object having been removed
- this.focusableElements = Keyboard.findFocusable(this.$element);
- afterAnimation();
- }
- });
- }
- // jQuery method of reveal
- else {
- if (this.options.overlay) {
- this.$overlay.show(0);
- }
- this.$element.show(this.options.showDelay);
- }
- // handle accessibility
- this.$element
- .attr({
- 'aria-hidden': false,
- 'tabindex': -1
- })
- .focus();
- Keyboard.trapFocus(this.$element);
- this._addGlobalClasses();
- this._addGlobalListeners();
- /**
- * Fires when the modal has successfully opened.
- * @event Reveal#open
- */
- this.$element.trigger('open.zf.reveal');
- }
- /**
- * Adds classes and listeners on document required by open modals.
- *
- * The following classes are added and updated:
- * - `.is-reveal-open` - Prevents the scroll on document
- * - `.zf-has-scroll` - Displays a disabled scrollbar on document if required like if the
- * scroll was not disabled. This prevent a "shift" of the page content due
- * the scrollbar disappearing when the modal opens.
- *
- * @private
- */
- _addGlobalClasses() {
- const updateScrollbarClass = () => {
- $('html').toggleClass('zf-has-scroll', !!($(document).height() > $(window).height()));
- };
- this.$element.on('resizeme.zf.trigger.revealScrollbarListener', () => updateScrollbarClass());
- updateScrollbarClass();
- $('html').addClass('is-reveal-open');
- }
- /**
- * Removes classes and listeners on document that were required by open modals.
- * @private
- */
- _removeGlobalClasses() {
- this.$element.off('resizeme.zf.trigger.revealScrollbarListener');
- $('html').removeClass('is-reveal-open');
- $('html').removeClass('zf-has-scroll');
- }
- /**
- * Adds extra event handlers for the body and window if necessary.
- * @private
- */
- _addGlobalListeners() {
- var _this = this;
- if(!this.$element) { return; } // If we're in the middle of cleanup, don't freak out
- this.focusableElements = 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) ||
- !$.contains(document, e.target)) { return; }
- _this.close();
- });
- }
- if (this.options.closeOnEsc) {
- $(window).on('keydown.zf.reveal', function(e) {
- Keyboard.handleKey(e, 'Reveal', {
- close: function() {
- if (_this.options.closeOnEsc) {
- _this.close();
- }
- }
- });
- });
- }
- }
- /**
- * Closes the modal.
- * @function
- * @fires Reveal#closed
- */
- close() {
- if (!this.isActive || !this.$element.is(':visible')) {
- return false;
- }
- var _this = this;
- // Motion UI method of hiding
- if (this.options.animationOut) {
- if (this.options.overlay) {
- Motion.animateOut(this.$overlay, 'fade-out');
- }
- Motion.animateOut(this.$element, this.options.animationOut, finishUp);
- }
- // jQuery method of hiding
- else {
- this.$element.hide(this.options.hideDelay);
- 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() {
- // Get the current top before the modal is closed and restore the scroll after.
- // TODO: use component properties instead of HTML properties
- // See https://github.com/zurb/foundation-sites/pull/10786
- var scrollTop = parseInt($("html").css("top"));
- if ($('.reveal:visible').length === 0) {
- _this._removeGlobalClasses(); // also remove .is-reveal-open from the html element when there is no opened reveal
- }
- Keyboard.releaseFocus(_this.$element);
- _this.$element.attr('aria-hidden', true);
- _this._enableScroll(scrollTop);
- /**
- * Fires when the modal is done closing.
- * @event Reveal#closed
- */
- _this.$element.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 deepLink and we did not switched to an other modal...
- if (_this.options.deepLink && window.location.hash === `#${this.id}`) {
- // Remove the history hash
- if (window.history.replaceState) {
- const urlWithoutHash = window.location.pathname + window.location.search;
- if (this.options.updateHistory) {
- window.history.pushState({}, '', urlWithoutHash); // remove the hash
- } else {
- window.history.replaceState('', document.title, urlWithoutHash);
- }
- } else {
- window.location.hash = '';
- }
- }
- this.$activeAnchor.focus();
- }
- /**
- * Toggles the open/closed state of a modal.
- * @function
- */
- toggle() {
- if (this.isActive) {
- this.close();
- } else {
- this.open();
- }
- };
- /**
- * Destroys an instance of a modal.
- * @function
- */
- _destroy() {
- if (this.options.overlay) {
- this.$element.appendTo($(this.options.appendTo)); // move $element outside of $overlay to prevent error unregisterPlugin()
- this.$overlay.hide().off().remove();
- }
- this.$element.hide().off();
- this.$anchor.off('.zf');
- $(window).off(`.zf.reveal:${this.id}`)
- if (this.onLoadListener) $(window).off(this.onLoadListener);
- if ($('.reveal:visible').length === 0) {
- this._removeGlobalClasses(); // also remove .is-reveal-open from the html element when there is no opened reveal
- }
- };
- }
- Reveal.defaults = {
- /**
- * Motion-UI class to use for animated elements. If none used, defaults to simple show/hide.
- * @option
- * @type {string}
- * @default ''
- */
- animationIn: '',
- /**
- * Motion-UI class to use for animated elements. If none used, defaults to simple show/hide.
- * @option
- * @type {string}
- * @default ''
- */
- animationOut: '',
- /**
- * Time, in ms, to delay the opening of a modal after a click if no animation used.
- * @option
- * @type {number}
- * @default 0
- */
- showDelay: 0,
- /**
- * Time, in ms, to delay the closing of a modal after a click if no animation used.
- * @option
- * @type {number}
- * @default 0
- */
- hideDelay: 0,
- /**
- * Allows a click on the body/overlay to close the modal.
- * @option
- * @type {boolean}
- * @default true
- */
- closeOnClick: true,
- /**
- * Allows the modal to close if the user presses the `ESCAPE` key.
- * @option
- * @type {boolean}
- * @default true
- */
- closeOnEsc: true,
- /**
- * If true, allows multiple modals to be displayed at once.
- * @option
- * @type {boolean}
- * @default false
- */
- multipleOpened: false,
- /**
- * Distance, in pixels, the modal should push down from the top of the screen.
- * @option
- * @type {number|string}
- * @default auto
- */
- vOffset: 'auto',
- /**
- * Distance, in pixels, the modal should push in from the side of the screen.
- * @option
- * @type {number|string}
- * @default auto
- */
- hOffset: 'auto',
- /**
- * Allows the modal to be fullscreen, completely blocking out the rest of the view. JS checks for this as well.
- * @option
- * @type {boolean}
- * @default false
- */
- fullScreen: false,
- /**
- * Allows the modal to generate an overlay div, which will cover the view when modal opens.
- * @option
- * @type {boolean}
- * @default 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
- * @type {boolean}
- * @default false
- */
- resetOnClose: false,
- /**
- * Link the location hash to the modal.
- * Set the location hash when the modal is opened/closed, and open/close the modal when the location changes.
- * @option
- * @type {boolean}
- * @default false
- */
- deepLink: false,
- /**
- * If `deepLink` is enabled, update the browser history with the open modal
- * @option
- * @default false
- */
- updateHistory: false,
- /**
- * Allows the modal to append to custom div.
- * @option
- * @type {string}
- * @default "body"
- */
- appendTo: "body",
- /**
- * Allows adding additional class names to the reveal overlay.
- * @option
- * @type {string}
- * @default ''
- */
- additionalOverlayClasses: ''
- };
- export {Reveal};
|