UNPKG

15.4 kBJavaScriptView Raw
1'use strict';
2
3import $ from 'jquery';
4import { Keyboard } from './foundation.util.keyboard';
5import { MediaQuery } from './foundation.util.mediaQuery';
6import { Motion } from './foundation.util.motion';
7import { Plugin } from './foundation.plugin';
8import { Triggers } from './foundation.util.triggers';
9
10/**
11 * Reveal module.
12 * @module foundation.reveal
13 * @requires foundation.util.keyboard
14 * @requires foundation.util.triggers
15 * @requires foundation.util.mediaQuery
16 * @requires foundation.util.motion if using animations
17 */
18
19class Reveal extends Plugin {
20 /**
21 * Creates a new instance of Reveal.
22 * @class
23 * @name Reveal
24 * @param {jQuery} element - jQuery object to use for the modal.
25 * @param {Object} options - optional parameters.
26 */
27 _setup(element, options) {
28 this.$element = element;
29 this.options = $.extend({}, Reveal.defaults, this.$element.data(), options);
30 this.className = 'Reveal'; // ie9 back compat
31 this._init();
32
33 // Triggers init is idempotent, just need to make sure it is initialized
34 Triggers.init($);
35
36 Keyboard.register('Reveal', {
37 'ESCAPE': 'close',
38 });
39 }
40
41 /**
42 * Initializes the modal by adding the overlay and close buttons, (if selected).
43 * @private
44 */
45 _init() {
46 MediaQuery._init();
47 this.id = this.$element.attr('id');
48 this.isActive = false;
49 this.cached = {mq: MediaQuery.current};
50 this.isMobile = mobileSniff();
51
52 this.$anchor = $(`[data-open="${this.id}"]`).length ? $(`[data-open="${this.id}"]`) : $(`[data-toggle="${this.id}"]`);
53 this.$anchor.attr({
54 'aria-controls': this.id,
55 'aria-haspopup': true,
56 'tabindex': 0
57 });
58
59 if (this.options.fullScreen || this.$element.hasClass('full')) {
60 this.options.fullScreen = true;
61 this.options.overlay = false;
62 }
63 if (this.options.overlay && !this.$overlay) {
64 this.$overlay = this._makeOverlay(this.id);
65 }
66
67 this.$element.attr({
68 'role': 'dialog',
69 'aria-hidden': true,
70 'data-yeti-box': this.id,
71 'data-resize': this.id
72 });
73
74 if(this.$overlay) {
75 this.$element.detach().appendTo(this.$overlay);
76 } else {
77 this.$element.detach().appendTo($(this.options.appendTo));
78 this.$element.addClass('without-overlay');
79 }
80 this._events();
81 if (this.options.deepLink && window.location.hash === ( `#${this.id}`)) {
82 $(window).one('load.zf.reveal', this.open.bind(this));
83 }
84 }
85
86 /**
87 * Creates an overlay div to display behind the modal.
88 * @private
89 */
90 _makeOverlay() {
91 var additionalOverlayClasses = '';
92
93 if (this.options.additionalOverlayClasses) {
94 additionalOverlayClasses = ' ' + this.options.additionalOverlayClasses;
95 }
96
97 return $('<div></div>')
98 .addClass('reveal-overlay' + additionalOverlayClasses)
99 .appendTo(this.options.appendTo);
100 }
101
102 /**
103 * Updates position of modal
104 * TODO: Figure out if we actually need to cache these values or if it doesn't matter
105 * @private
106 */
107 _updatePosition() {
108 var width = this.$element.outerWidth();
109 var outerWidth = $(window).width();
110 var height = this.$element.outerHeight();
111 var outerHeight = $(window).height();
112 var left, top;
113 if (this.options.hOffset === 'auto') {
114 left = parseInt((outerWidth - width) / 2, 10);
115 } else {
116 left = parseInt(this.options.hOffset, 10);
117 }
118 if (this.options.vOffset === 'auto') {
119 if (height > outerHeight) {
120 top = parseInt(Math.min(100, outerHeight / 10), 10);
121 } else {
122 top = parseInt((outerHeight - height) / 4, 10);
123 }
124 } else {
125 top = parseInt(this.options.vOffset, 10);
126 }
127 this.$element.css({top: top + 'px'});
128 // only worry about left if we don't have an overlay or we havea horizontal offset,
129 // otherwise we're perfectly in the middle
130 if(!this.$overlay || (this.options.hOffset !== 'auto')) {
131 this.$element.css({left: left + 'px'});
132 this.$element.css({margin: '0px'});
133 }
134
135 }
136
137 /**
138 * Adds event handlers for the modal.
139 * @private
140 */
141 _events() {
142 var _this = this;
143
144 this.$element.on({
145 'open.zf.trigger': this.open.bind(this),
146 'close.zf.trigger': (event, $element) => {
147 if ((event.target === _this.$element[0]) ||
148 ($(event.target).parents('[data-closable]')[0] === $element)) { // only close reveal when it's explicitly called
149 return this.close.apply(this);
150 }
151 },
152 'toggle.zf.trigger': this.toggle.bind(this),
153 'resizeme.zf.trigger': function() {
154 _this._updatePosition();
155 }
156 });
157
158 if (this.options.closeOnClick && this.options.overlay) {
159 this.$overlay.off('.zf.reveal').on('click.zf.reveal', function(e) {
160 if (e.target === _this.$element[0] ||
161 $.contains(_this.$element[0], e.target) ||
162 !$.contains(document, e.target)) {
163 return;
164 }
165 _this.close();
166 });
167 }
168 if (this.options.deepLink) {
169 $(window).on(`popstate.zf.reveal:${this.id}`, this._handleState.bind(this));
170 }
171 }
172
173 /**
174 * Handles modal methods on back/forward button clicks or any other event that triggers popstate.
175 * @private
176 */
177 _handleState(e) {
178 if(window.location.hash === ( '#' + this.id) && !this.isActive){ this.open(); }
179 else{ this.close(); }
180 }
181
182
183 /**
184 * Opens the modal controlled by `this.$anchor`, and closes all others by default.
185 * @function
186 * @fires Reveal#closeme
187 * @fires Reveal#open
188 */
189 open() {
190 // either update or replace browser history
191 if (this.options.deepLink) {
192 var hash = `#${this.id}`;
193
194 if (window.history.pushState) {
195 if (this.options.updateHistory) {
196 window.history.pushState({}, '', hash);
197 } else {
198 window.history.replaceState({}, '', hash);
199 }
200 } else {
201 window.location.hash = hash;
202 }
203 }
204
205 this.isActive = true;
206
207 // Make elements invisible, but remove display: none so we can get size and positioning
208 this.$element
209 .css({ 'visibility': 'hidden' })
210 .show()
211 .scrollTop(0);
212 if (this.options.overlay) {
213 this.$overlay.css({'visibility': 'hidden'}).show();
214 }
215
216 this._updatePosition();
217
218 this.$element
219 .hide()
220 .css({ 'visibility': '' });
221
222 if(this.$overlay) {
223 this.$overlay.css({'visibility': ''}).hide();
224 if(this.$element.hasClass('fast')) {
225 this.$overlay.addClass('fast');
226 } else if (this.$element.hasClass('slow')) {
227 this.$overlay.addClass('slow');
228 }
229 }
230
231
232 if (!this.options.multipleOpened) {
233 /**
234 * Fires immediately before the modal opens.
235 * Closes any other modals that are currently open
236 * @event Reveal#closeme
237 */
238 this.$element.trigger('closeme.zf.reveal', this.id);
239 }
240
241 var _this = this;
242
243 function addRevealOpenClasses() {
244 if (_this.isMobile) {
245 if(!_this.originalScrollPos) {
246 _this.originalScrollPos = window.pageYOffset;
247 }
248 $('html, body').addClass('is-reveal-open');
249 }
250 else {
251 $('body').addClass('is-reveal-open');
252 }
253 }
254 // Motion UI method of reveal
255 if (this.options.animationIn) {
256 function afterAnimation(){
257 _this.$element
258 .attr({
259 'aria-hidden': false,
260 'tabindex': -1
261 })
262 .focus();
263 addRevealOpenClasses();
264 Keyboard.trapFocus(_this.$element);
265 }
266 if (this.options.overlay) {
267 Motion.animateIn(this.$overlay, 'fade-in');
268 }
269 Motion.animateIn(this.$element, this.options.animationIn, () => {
270 if(this.$element) { // protect against object having been removed
271 this.focusableElements = Keyboard.findFocusable(this.$element);
272 afterAnimation();
273 }
274 });
275 }
276 // jQuery method of reveal
277 else {
278 if (this.options.overlay) {
279 this.$overlay.show(0);
280 }
281 this.$element.show(this.options.showDelay);
282 }
283
284 // handle accessibility
285 this.$element
286 .attr({
287 'aria-hidden': false,
288 'tabindex': -1
289 })
290 .focus();
291 Keyboard.trapFocus(this.$element);
292
293 addRevealOpenClasses();
294
295 this._extraHandlers();
296
297 /**
298 * Fires when the modal has successfully opened.
299 * @event Reveal#open
300 */
301 this.$element.trigger('open.zf.reveal');
302 }
303
304 /**
305 * Adds extra event handlers for the body and window if necessary.
306 * @private
307 */
308 _extraHandlers() {
309 var _this = this;
310 if(!this.$element) { return; } // If we're in the middle of cleanup, don't freak out
311 this.focusableElements = Keyboard.findFocusable(this.$element);
312
313 if (!this.options.overlay && this.options.closeOnClick && !this.options.fullScreen) {
314 $('body').on('click.zf.reveal', function(e) {
315 if (e.target === _this.$element[0] ||
316 $.contains(_this.$element[0], e.target) ||
317 !$.contains(document, e.target)) { return; }
318 _this.close();
319 });
320 }
321
322 if (this.options.closeOnEsc) {
323 $(window).on('keydown.zf.reveal', function(e) {
324 Keyboard.handleKey(e, 'Reveal', {
325 close: function() {
326 if (_this.options.closeOnEsc) {
327 _this.close();
328 }
329 }
330 });
331 });
332 }
333 }
334
335 /**
336 * Closes the modal.
337 * @function
338 * @fires Reveal#closed
339 */
340 close() {
341 if (!this.isActive || !this.$element.is(':visible')) {
342 return false;
343 }
344 var _this = this;
345
346 // Motion UI method of hiding
347 if (this.options.animationOut) {
348 if (this.options.overlay) {
349 Motion.animateOut(this.$overlay, 'fade-out');
350 }
351
352 Motion.animateOut(this.$element, this.options.animationOut, finishUp);
353 }
354 // jQuery method of hiding
355 else {
356 this.$element.hide(this.options.hideDelay);
357
358 if (this.options.overlay) {
359 this.$overlay.hide(0, finishUp);
360 }
361 else {
362 finishUp();
363 }
364 }
365
366 // Conditionals to remove extra event listeners added on open
367 if (this.options.closeOnEsc) {
368 $(window).off('keydown.zf.reveal');
369 }
370
371 if (!this.options.overlay && this.options.closeOnClick) {
372 $('body').off('click.zf.reveal');
373 }
374
375 this.$element.off('keydown.zf.reveal');
376
377 function finishUp() {
378 if (_this.isMobile) {
379 if ($('.reveal:visible').length === 0) {
380 $('html, body').removeClass('is-reveal-open');
381 }
382 if(_this.originalScrollPos) {
383 $('body').scrollTop(_this.originalScrollPos);
384 _this.originalScrollPos = null;
385 }
386 }
387 else {
388 if ($('.reveal:visible').length === 0) {
389 $('body').removeClass('is-reveal-open');
390 }
391 }
392
393
394 Keyboard.releaseFocus(_this.$element);
395
396 _this.$element.attr('aria-hidden', true);
397
398 /**
399 * Fires when the modal is done closing.
400 * @event Reveal#closed
401 */
402 _this.$element.trigger('closed.zf.reveal');
403 }
404
405 /**
406 * Resets the modal content
407 * This prevents a running video to keep going in the background
408 */
409 if (this.options.resetOnClose) {
410 this.$element.html(this.$element.html());
411 }
412
413 this.isActive = false;
414 if (_this.options.deepLink) {
415 if (window.history.replaceState) {
416 window.history.replaceState('', document.title, window.location.href.replace(`#${this.id}`, ''));
417 } else {
418 window.location.hash = '';
419 }
420 }
421
422 this.$anchor.focus();
423 }
424
425 /**
426 * Toggles the open/closed state of a modal.
427 * @function
428 */
429 toggle() {
430 if (this.isActive) {
431 this.close();
432 } else {
433 this.open();
434 }
435 };
436
437 /**
438 * Destroys an instance of a modal.
439 * @function
440 */
441 _destroy() {
442 if (this.options.overlay) {
443 this.$element.appendTo($(this.options.appendTo)); // move $element outside of $overlay to prevent error unregisterPlugin()
444 this.$overlay.hide().off().remove();
445 }
446 this.$element.hide().off();
447 this.$anchor.off('.zf');
448 $(window).off(`.zf.reveal:${this.id}`);
449 };
450}
451
452Reveal.defaults = {
453 /**
454 * Motion-UI class to use for animated elements. If none used, defaults to simple show/hide.
455 * @option
456 * @type {string}
457 * @default ''
458 */
459 animationIn: '',
460 /**
461 * Motion-UI class to use for animated elements. If none used, defaults to simple show/hide.
462 * @option
463 * @type {string}
464 * @default ''
465 */
466 animationOut: '',
467 /**
468 * Time, in ms, to delay the opening of a modal after a click if no animation used.
469 * @option
470 * @type {number}
471 * @default 0
472 */
473 showDelay: 0,
474 /**
475 * Time, in ms, to delay the closing of a modal after a click if no animation used.
476 * @option
477 * @type {number}
478 * @default 0
479 */
480 hideDelay: 0,
481 /**
482 * Allows a click on the body/overlay to close the modal.
483 * @option
484 * @type {boolean}
485 * @default true
486 */
487 closeOnClick: true,
488 /**
489 * Allows the modal to close if the user presses the `ESCAPE` key.
490 * @option
491 * @type {boolean}
492 * @default true
493 */
494 closeOnEsc: true,
495 /**
496 * If true, allows multiple modals to be displayed at once.
497 * @option
498 * @type {boolean}
499 * @default false
500 */
501 multipleOpened: false,
502 /**
503 * Distance, in pixels, the modal should push down from the top of the screen.
504 * @option
505 * @type {number|string}
506 * @default auto
507 */
508 vOffset: 'auto',
509 /**
510 * Distance, in pixels, the modal should push in from the side of the screen.
511 * @option
512 * @type {number|string}
513 * @default auto
514 */
515 hOffset: 'auto',
516 /**
517 * Allows the modal to be fullscreen, completely blocking out the rest of the view. JS checks for this as well.
518 * @option
519 * @type {boolean}
520 * @default false
521 */
522 fullScreen: false,
523 /**
524 * Percentage of screen height the modal should push up from the bottom of the view.
525 * @option
526 * @type {number}
527 * @default 10
528 */
529 btmOffsetPct: 10,
530 /**
531 * Allows the modal to generate an overlay div, which will cover the view when modal opens.
532 * @option
533 * @type {boolean}
534 * @default true
535 */
536 overlay: true,
537 /**
538 * 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.
539 * @option
540 * @type {boolean}
541 * @default false
542 */
543 resetOnClose: false,
544 /**
545 * 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.
546 * @option
547 * @type {boolean}
548 * @default false
549 */
550 deepLink: false,
551 /**
552 * Update the browser history with the open modal
553 * @option
554 * @default false
555 */
556 updateHistory: false,
557 /**
558 * Allows the modal to append to custom div.
559 * @option
560 * @type {string}
561 * @default "body"
562 */
563 appendTo: "body",
564 /**
565 * Allows adding additional class names to the reveal overlay.
566 * @option
567 * @type {string}
568 * @default ''
569 */
570 additionalOverlayClasses: ''
571};
572
573function iPhoneSniff() {
574 return /iP(ad|hone|od).*OS/.test(window.navigator.userAgent);
575}
576
577function androidSniff() {
578 return /Android/.test(window.navigator.userAgent);
579}
580
581function mobileSniff() {
582 return iPhoneSniff() || androidSniff();
583}
584
585export {Reveal};