1 | 'use strict';
|
2 |
|
3 | import $ from 'jquery';
|
4 | import { Keyboard } from './foundation.util.keyboard';
|
5 | import { MediaQuery } from './foundation.util.mediaQuery';
|
6 | import { transitionend } from './foundation.util.core';
|
7 | import { Plugin } from './foundation.plugin';
|
8 |
|
9 | import { Triggers } from './foundation.util.triggers';
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 | class OffCanvas extends Plugin {
|
20 | |
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 | _setup(element, options) {
|
29 | this.className = 'OffCanvas';
|
30 | this.$element = element;
|
31 | this.options = $.extend({}, OffCanvas.defaults, this.$element.data(), options);
|
32 | this.contentClasses = { base: [], reveal: [] };
|
33 | this.$lastTrigger = $();
|
34 | this.$triggers = $();
|
35 | this.position = 'left';
|
36 | this.$content = $();
|
37 | this.nested = !!(this.options.nested);
|
38 |
|
39 |
|
40 | $(['push', 'overlap']).each((index, val) => {
|
41 | this.contentClasses.base.push('has-transition-'+val);
|
42 | });
|
43 | $(['left', 'right', 'top', 'bottom']).each((index, val) => {
|
44 | this.contentClasses.base.push('has-position-'+val);
|
45 | this.contentClasses.reveal.push('has-reveal-'+val);
|
46 | });
|
47 |
|
48 |
|
49 | Triggers.init($);
|
50 | MediaQuery._init();
|
51 |
|
52 | this._init();
|
53 | this._events();
|
54 |
|
55 | Keyboard.register('OffCanvas', {
|
56 | 'ESCAPE': 'close'
|
57 | });
|
58 |
|
59 | }
|
60 |
|
61 | |
62 |
|
63 |
|
64 |
|
65 |
|
66 | _init() {
|
67 | var id = this.$element.attr('id');
|
68 |
|
69 | this.$element.attr('aria-hidden', 'true');
|
70 |
|
71 |
|
72 | if (this.options.contentId) {
|
73 | this.$content = $('#'+this.options.contentId);
|
74 | } else if (this.$element.siblings('[data-off-canvas-content]').length) {
|
75 | this.$content = this.$element.siblings('[data-off-canvas-content]').first();
|
76 | } else {
|
77 | this.$content = this.$element.closest('[data-off-canvas-content]').first();
|
78 | }
|
79 |
|
80 | if (!this.options.contentId) {
|
81 |
|
82 | this.nested = this.$element.siblings('[data-off-canvas-content]').length === 0;
|
83 |
|
84 | } else if (this.options.contentId && this.options.nested === null) {
|
85 |
|
86 |
|
87 | console.warn('Remember to use the nested option if using the content ID option!');
|
88 | }
|
89 |
|
90 | if (this.nested === true) {
|
91 |
|
92 | this.options.transition = 'overlap';
|
93 |
|
94 | this.$element.removeClass('is-transition-push');
|
95 | }
|
96 |
|
97 | this.$element.addClass(`is-transition-${this.options.transition} is-closed`);
|
98 |
|
99 |
|
100 | this.$triggers = $(document)
|
101 | .find('[data-open="'+id+'"], [data-close="'+id+'"], [data-toggle="'+id+'"]')
|
102 | .attr('aria-expanded', 'false')
|
103 | .attr('aria-controls', id);
|
104 |
|
105 |
|
106 | this.position = this.$element.is('.position-left, .position-top, .position-right, .position-bottom') ? this.$element.attr('class').match(/position\-(left|top|right|bottom)/)[1] : this.position;
|
107 |
|
108 |
|
109 | if (this.options.contentOverlay === true) {
|
110 | var overlay = document.createElement('div');
|
111 | var overlayPosition = $(this.$element).css("position") === 'fixed' ? 'is-overlay-fixed' : 'is-overlay-absolute';
|
112 | overlay.setAttribute('class', 'js-off-canvas-overlay ' + overlayPosition);
|
113 | this.$overlay = $(overlay);
|
114 | if(overlayPosition === 'is-overlay-fixed') {
|
115 | $(this.$overlay).insertAfter(this.$element);
|
116 | } else {
|
117 | this.$content.append(this.$overlay);
|
118 | }
|
119 | }
|
120 |
|
121 | this.options.isRevealed = this.options.isRevealed || new RegExp(this.options.revealClass, 'g').test(this.$element[0].className);
|
122 |
|
123 | if (this.options.isRevealed === true) {
|
124 | this.options.revealOn = this.options.revealOn || this.$element[0].className.match(/(reveal-for-medium|reveal-for-large)/g)[0].split('-')[2];
|
125 | this._setMQChecker();
|
126 | }
|
127 |
|
128 | if (this.options.transitionTime) {
|
129 | this.$element.css('transition-duration', this.options.transitionTime);
|
130 | }
|
131 |
|
132 |
|
133 | this._removeContentClasses();
|
134 | }
|
135 |
|
136 | |
137 |
|
138 |
|
139 |
|
140 |
|
141 | _events() {
|
142 | this.$element.off('.zf.trigger .zf.offcanvas').on({
|
143 | 'open.zf.trigger': this.open.bind(this),
|
144 | 'close.zf.trigger': this.close.bind(this),
|
145 | 'toggle.zf.trigger': this.toggle.bind(this),
|
146 | 'keydown.zf.offcanvas': this._handleKeyboard.bind(this)
|
147 | });
|
148 |
|
149 | if (this.options.closeOnClick === true) {
|
150 | var $target = this.options.contentOverlay ? this.$overlay : this.$content;
|
151 | $target.on({'click.zf.offcanvas': this.close.bind(this)});
|
152 | }
|
153 | }
|
154 |
|
155 | |
156 |
|
157 |
|
158 |
|
159 | _setMQChecker() {
|
160 | var _this = this;
|
161 |
|
162 | $(window).on('changed.zf.mediaquery', function() {
|
163 | if (MediaQuery.atLeast(_this.options.revealOn)) {
|
164 | _this.reveal(true);
|
165 | } else {
|
166 | _this.reveal(false);
|
167 | }
|
168 | }).one('load.zf.offcanvas', function() {
|
169 | if (MediaQuery.atLeast(_this.options.revealOn)) {
|
170 | _this.reveal(true);
|
171 | }
|
172 | });
|
173 | }
|
174 |
|
175 | |
176 |
|
177 |
|
178 |
|
179 |
|
180 | _removeContentClasses(hasReveal) {
|
181 | this.$content.removeClass(this.contentClasses.base.join(' '));
|
182 | if (hasReveal === true) {
|
183 | this.$content.removeClass(this.contentClasses.reveal.join(' '));
|
184 | }
|
185 | }
|
186 |
|
187 | |
188 |
|
189 |
|
190 |
|
191 |
|
192 |
|
193 | _addContentClasses(hasReveal) {
|
194 | this._removeContentClasses();
|
195 | this.$content.addClass(`has-transition-${this.options.transition} has-position-${this.position}`);
|
196 | if (hasReveal === true) {
|
197 | this.$content.addClass(`has-reveal-${this.position}`);
|
198 | }
|
199 | }
|
200 |
|
201 | |
202 |
|
203 |
|
204 |
|
205 |
|
206 | reveal(isRevealed) {
|
207 | if (isRevealed) {
|
208 | this.close();
|
209 | this.isRevealed = true;
|
210 | this.$element.attr('aria-hidden', 'false');
|
211 | this.$element.off('open.zf.trigger toggle.zf.trigger');
|
212 | this.$element.removeClass('is-closed');
|
213 | } else {
|
214 | this.isRevealed = false;
|
215 | this.$element.attr('aria-hidden', 'true');
|
216 | this.$element.off('open.zf.trigger toggle.zf.trigger').on({
|
217 | 'open.zf.trigger': this.open.bind(this),
|
218 | 'toggle.zf.trigger': this.toggle.bind(this)
|
219 | });
|
220 | this.$element.addClass('is-closed');
|
221 | }
|
222 | this._addContentClasses(isRevealed);
|
223 | }
|
224 |
|
225 | |
226 |
|
227 |
|
228 |
|
229 | _stopScrolling(event) {
|
230 | return false;
|
231 | }
|
232 |
|
233 |
|
234 |
|
235 | _recordScrollable(event) {
|
236 | let elem = this;
|
237 |
|
238 |
|
239 | if (elem.scrollHeight !== elem.clientHeight) {
|
240 |
|
241 | if (elem.scrollTop === 0) {
|
242 | elem.scrollTop = 1;
|
243 | }
|
244 |
|
245 | if (elem.scrollTop === elem.scrollHeight - elem.clientHeight) {
|
246 | elem.scrollTop = elem.scrollHeight - elem.clientHeight - 1;
|
247 | }
|
248 | }
|
249 | elem.allowUp = elem.scrollTop > 0;
|
250 | elem.allowDown = elem.scrollTop < (elem.scrollHeight - elem.clientHeight);
|
251 | elem.lastY = event.originalEvent.pageY;
|
252 | }
|
253 |
|
254 | _stopScrollPropagation(event) {
|
255 | let elem = this;
|
256 | let up = event.pageY < elem.lastY;
|
257 | let down = !up;
|
258 | elem.lastY = event.pageY;
|
259 |
|
260 | if((up && elem.allowUp) || (down && elem.allowDown)) {
|
261 | event.stopPropagation();
|
262 | } else {
|
263 | event.preventDefault();
|
264 | }
|
265 | }
|
266 |
|
267 | |
268 |
|
269 |
|
270 |
|
271 |
|
272 |
|
273 |
|
274 | open(event, trigger) {
|
275 | if (this.$element.hasClass('is-open') || this.isRevealed) { return; }
|
276 | var _this = this;
|
277 |
|
278 | if (trigger) {
|
279 | this.$lastTrigger = trigger;
|
280 | }
|
281 |
|
282 | if (this.options.forceTo === 'top') {
|
283 | window.scrollTo(0, 0);
|
284 | } else if (this.options.forceTo === 'bottom') {
|
285 | window.scrollTo(0,document.body.scrollHeight);
|
286 | }
|
287 |
|
288 | if (this.options.transitionTime && this.options.transition !== 'overlap') {
|
289 | this.$element.siblings('[data-off-canvas-content]').css('transition-duration', this.options.transitionTime);
|
290 | } else {
|
291 | this.$element.siblings('[data-off-canvas-content]').css('transition-duration', '');
|
292 | }
|
293 |
|
294 | |
295 |
|
296 |
|
297 |
|
298 | this.$element.addClass('is-open').removeClass('is-closed');
|
299 |
|
300 | this.$triggers.attr('aria-expanded', 'true');
|
301 | this.$element.attr('aria-hidden', 'false')
|
302 | .trigger('opened.zf.offcanvas');
|
303 |
|
304 | this.$content.addClass('is-open-' + this.position);
|
305 |
|
306 |
|
307 | if (this.options.contentScroll === false) {
|
308 | $('body').addClass('is-off-canvas-open').on('touchmove', this._stopScrolling);
|
309 | this.$element.on('touchstart', this._recordScrollable);
|
310 | this.$element.on('touchmove', this._stopScrollPropagation);
|
311 | }
|
312 |
|
313 | if (this.options.contentOverlay === true) {
|
314 | this.$overlay.addClass('is-visible');
|
315 | }
|
316 |
|
317 | if (this.options.closeOnClick === true && this.options.contentOverlay === true) {
|
318 | this.$overlay.addClass('is-closable');
|
319 | }
|
320 |
|
321 | if (this.options.autoFocus === true) {
|
322 | this.$element.one(transitionend(this.$element), function() {
|
323 | if (!_this.$element.hasClass('is-open')) {
|
324 | return;
|
325 | }
|
326 | var canvasFocus = _this.$element.find('[data-autofocus]');
|
327 | if (canvasFocus.length) {
|
328 | canvasFocus.eq(0).focus();
|
329 | } else {
|
330 | _this.$element.find('a, button').eq(0).focus();
|
331 | }
|
332 | });
|
333 | }
|
334 |
|
335 | if (this.options.trapFocus === true) {
|
336 | this.$content.attr('tabindex', '-1');
|
337 | Keyboard.trapFocus(this.$element);
|
338 | }
|
339 |
|
340 | this._addContentClasses();
|
341 | }
|
342 |
|
343 | |
344 |
|
345 |
|
346 |
|
347 |
|
348 |
|
349 | close(cb) {
|
350 | if (!this.$element.hasClass('is-open') || this.isRevealed) { return; }
|
351 |
|
352 | var _this = this;
|
353 |
|
354 | this.$element.removeClass('is-open');
|
355 |
|
356 | this.$element.attr('aria-hidden', 'true')
|
357 | |
358 |
|
359 |
|
360 |
|
361 | .trigger('closed.zf.offcanvas');
|
362 |
|
363 | this.$content.removeClass('is-open-left is-open-top is-open-right is-open-bottom');
|
364 |
|
365 |
|
366 | if (this.options.contentScroll === false) {
|
367 | $('body').removeClass('is-off-canvas-open').off('touchmove', this._stopScrolling);
|
368 | this.$element.off('touchstart', this._recordScrollable);
|
369 | this.$element.off('touchmove', this._stopScrollPropagation);
|
370 | }
|
371 |
|
372 | if (this.options.contentOverlay === true) {
|
373 | this.$overlay.removeClass('is-visible');
|
374 | }
|
375 |
|
376 | if (this.options.closeOnClick === true && this.options.contentOverlay === true) {
|
377 | this.$overlay.removeClass('is-closable');
|
378 | }
|
379 |
|
380 | this.$triggers.attr('aria-expanded', 'false');
|
381 |
|
382 | if (this.options.trapFocus === true) {
|
383 | this.$content.removeAttr('tabindex');
|
384 | Keyboard.releaseFocus(this.$element);
|
385 | }
|
386 |
|
387 |
|
388 | this.$element.one(transitionend(this.$element), function(e) {
|
389 | _this.$element.addClass('is-closed');
|
390 | _this._removeContentClasses();
|
391 | });
|
392 | }
|
393 |
|
394 | |
395 |
|
396 |
|
397 |
|
398 |
|
399 |
|
400 | toggle(event, trigger) {
|
401 | if (this.$element.hasClass('is-open')) {
|
402 | this.close(event, trigger);
|
403 | }
|
404 | else {
|
405 | this.open(event, trigger);
|
406 | }
|
407 | }
|
408 |
|
409 | |
410 |
|
411 |
|
412 |
|
413 |
|
414 | _handleKeyboard(e) {
|
415 | Keyboard.handleKey(e, 'OffCanvas', {
|
416 | close: () => {
|
417 | this.close();
|
418 | this.$lastTrigger.focus();
|
419 | return true;
|
420 | },
|
421 | handled: () => {
|
422 | e.stopPropagation();
|
423 | e.preventDefault();
|
424 | }
|
425 | });
|
426 | }
|
427 |
|
428 | |
429 |
|
430 |
|
431 |
|
432 | _destroy() {
|
433 | this.close();
|
434 | this.$element.off('.zf.trigger .zf.offcanvas');
|
435 | this.$overlay.off('.zf.offcanvas');
|
436 | }
|
437 | }
|
438 |
|
439 | OffCanvas.defaults = {
|
440 | |
441 |
|
442 |
|
443 |
|
444 |
|
445 |
|
446 | closeOnClick: true,
|
447 |
|
448 | |
449 |
|
450 |
|
451 |
|
452 |
|
453 |
|
454 | contentOverlay: true,
|
455 |
|
456 | |
457 |
|
458 |
|
459 |
|
460 |
|
461 |
|
462 | contentId: null,
|
463 |
|
464 | |
465 |
|
466 |
|
467 |
|
468 |
|
469 |
|
470 | nested: null,
|
471 |
|
472 | |
473 |
|
474 |
|
475 |
|
476 |
|
477 |
|
478 | contentScroll: true,
|
479 |
|
480 | |
481 |
|
482 |
|
483 |
|
484 |
|
485 |
|
486 | transitionTime: null,
|
487 |
|
488 | |
489 |
|
490 |
|
491 |
|
492 |
|
493 |
|
494 | transition: 'push',
|
495 |
|
496 | |
497 |
|
498 |
|
499 |
|
500 |
|
501 |
|
502 | forceTo: null,
|
503 |
|
504 | |
505 |
|
506 |
|
507 |
|
508 |
|
509 |
|
510 | isRevealed: false,
|
511 |
|
512 | |
513 |
|
514 |
|
515 |
|
516 |
|
517 |
|
518 | revealOn: null,
|
519 |
|
520 | |
521 |
|
522 |
|
523 |
|
524 |
|
525 |
|
526 | autoFocus: true,
|
527 |
|
528 | |
529 |
|
530 |
|
531 |
|
532 |
|
533 |
|
534 |
|
535 | revealClass: 'reveal-for-',
|
536 |
|
537 | |
538 |
|
539 |
|
540 |
|
541 |
|
542 |
|
543 | trapFocus: false
|
544 | }
|
545 |
|
546 | export {OffCanvas};
|