1/** 2 * OffCanvas module. 3 * @module foundation.offcanvas 4 * @requires foundation.util.triggers 5 * @requires foundation.util.motion 6 */ 7!function($, Foundation) { 8 9'use strict'; 10 11/** 12 * Creates a new instance of an off-canvas wrapper. 13 * @class 14 * @fires OffCanvas#init 15 * @param {Object} element - jQuery object to initialize. 16 * @param {Object} options - Overrides to the default plugin settings. 17 */ 18function OffCanvas(element, options) { 19 this.$element = element; 20 this.options = $.extend({}, OffCanvas.defaults, this.$element.data(), options); 21 this.$lastTrigger = $(); 22 23 this._init(); 24 this._events(); 25 26 Foundation.registerPlugin(this, 'OffCanvas'); 27} 28 29OffCanvas.defaults = { 30 /** 31 * Allow the user to click outside of the menu to close it. 32 * @option 33 * @example true 34 */ 35 closeOnClick: true, 36 /** 37 * Amount of time in ms the open and close transition requires. If none selected, pulls from body style. 38 * @option 39 * @example 500 40 */ 41 transitionTime: 0, 42 /** 43 * Direction the offcanvas opens from. Determines class applied to body. 44 * @option 45 * @example left 46 */ 47 position: 'left', 48 /** 49 * Force the page to scroll to top on open. 50 */ 51 forceTop: true, 52 /** 53 * Allow the offcanvas to be sticky while open. Does nothing if Sass option `$maincontent-prevent-scroll === true`. 54 * Performance in Safari OSX/iOS is not great. 55 */ 56 // isSticky: false, 57 /** 58 * Allow the offcanvas to remain open for certain breakpoints. Can be used with `isSticky`. 59 * @option 60 * @example false 61 */ 62 isRevealed: false, 63 /** 64 * Breakpoint at which to reveal. JS will use a RegExp to target standard classes, if changing classnames, pass your class @`revealClass`. 65 * @option 66 * @example reveal-for-large 67 */ 68 revealOn: null, 69 /** 70 * Force focus to the offcanvas on open. If true, will focus the opening trigger on close. 71 * @option 72 * @example true 73 */ 74 autoFocus: true, 75 /** 76 * Class used to force an offcanvas to remain open. Foundation defaults for this are `reveal-for-large` & `reveal-for-medium`. 77 * @option 78 * TODO improve the regex testing for this. 79 * @example reveal-for-large 80 */ 81 revealClass: 'reveal-for-', 82 /** 83 * Triggers optional focus trapping when opening an offcanvas. Sets tabindex of [data-off-canvas-content] to -1 for accessibility purposes. 84 * @option 85 * @example true 86 */ 87 trapFocus: false 88}; 89 90/** 91 * Initializes the off-canvas wrapper by adding the exit overlay (if needed). 92 * @function 93 * @private 94 */ 95OffCanvas.prototype._init = function() { 96 var id = this.$element.attr('id'); 97 98 this.$element.attr('aria-hidden', 'true'); 99 100 // Find triggers that affect this element and add aria-expanded to them 101 $(document) 102 .find('[data-open="'+id+'"], [data-close="'+id+'"], [data-toggle="'+id+'"]') 103 .attr('aria-expanded', 'false') 104 .attr('aria-controls', id); 105 106 // Add a close trigger over the body if necessary 107 if (this.options.closeOnClick){ 108 if($('.js-off-canvas-exit').length){ 109 this.$exiter = $('.js-off-canvas-exit'); 110 }else{ 111 var exiter = document.createElement('div'); 112 exiter.setAttribute('class', 'js-off-canvas-exit'); 113 $('[data-off-canvas-content]').append(exiter); 114 115 this.$exiter = $(exiter); 116 } 117 } 118 119 this.options.isRevealed = this.options.isRevealed || new RegExp(this.options.revealClass, 'g').test(this.$element[0].className); 120 121 if(this.options.isRevealed){ 122 this.options.revealOn = this.options.revealOn || this.$element[0].className.match(/(reveal-for-medium|reveal-for-large)/g)[0].split('-')[2]; 123 this._setMQChecker(); 124 } 125 if(!this.options.transitionTime){ 126 this.options.transitionTime = parseFloat(window.getComputedStyle($('[data-off-canvas-wrapper]')[0]).transitionDuration) * 1000; 127 } 128}; 129 130/** 131 * Adds event handlers to the off-canvas wrapper and the exit overlay. 132 * @function 133 * @private 134 */ 135OffCanvas.prototype._events = function() { 136 this.$element.off('.zf.trigger .zf.offcanvas').on({ 137 'open.zf.trigger': this.open.bind(this), 138 'close.zf.trigger': this.close.bind(this), 139 'toggle.zf.trigger': this.toggle.bind(this), 140 'keydown.zf.offcanvas': this._handleKeyboard.bind(this) 141 }); 142 143 if (this.$exiter.length) { 144 var _this = this; 145 this.$exiter.on({'click.zf.offcanvas': this.close.bind(this)}); 146 } 147}; 148/** 149 * Applies event listener for elements that will reveal at certain breakpoints. 150 * @private 151 */ 152OffCanvas.prototype._setMQChecker = function(){ 153 var _this = this; 154 155 $(window).on('changed.zf.mediaquery', function(){ 156 if(Foundation.MediaQuery.atLeast(_this.options.revealOn)){ 157 _this.reveal(true); 158 }else{ 159 _this.reveal(false); 160 } 161 }).one('load.zf.offcanvas', function(){ 162 if(Foundation.MediaQuery.atLeast(_this.options.revealOn)){ 163 _this.reveal(true); 164 } 165 }); 166}; 167/**
168 * Handles the revealing/hiding the off-canvas at breakpoints, not the same as open. 169 * @param {Boolean} isRevealed - true if element should be revealed. 170 * @function 171 */ 172OffCanvas.prototype.reveal = function(isRevealed){ 173 var $closer = this.$element.find('[data-close]'); 174 if(isRevealed){ 175 this.close(); 176 this.isRevealed = true; 177 // if(!this.options.forceTop){ 178 // var scrollPos = parseInt(window.pageYOffset); 179 // this.$element[0].style.transform = 'translate(0,' + scrollPos + 'px)'; 180 // } 181 // if(this.options.isSticky){ this._stick(); } 182 this.$element.off('open.zf.trigger toggle.zf.trigger'); 183 if($closer.length){ $closer.hide(); } 184 }else{ 185 this.isRevealed = false; 186 // if(this.options.isSticky || !this.options.forceTop){ 187 // this.$element[0].style.transform = ''; 188 // $(window).off('scroll.zf.offcanvas'); 189 // } 190 this.$element.on({ 191 'open.zf.trigger': this.open.bind(this), 192 'toggle.zf.trigger': this.toggle.bind(this) 193 }); 194 if($closer.length){ 195 $closer.show(); 196 } 197 } 198}; 199 200/** 201 * Opens the off-canvas menu. 202 * @function 203 * @param {Object} event - Event object passed from listener. 204 * @param {jQuery} trigger - element that triggered the off-canvas to open. 205 * @fires OffCanvas#opened 206 */ 207OffCanvas.prototype.open = function(event, trigger) { 208 if (this.$element.hasClass('is-open') || this.isRevealed){ return; } 209 var _this = this, 210 $body = $(document.body); 211 $('body').scrollTop(0); 212 // window.pageYOffset = 0; 213 214 // if(!this.options.forceTop){ 215 // var scrollPos = parseInt(window.pageYOffset); 216 // this.$element[0].style.transform = 'translate(0,' + scrollPos + 'px)'; 217 // if(this.$exiter.length){ 218 // this.$exiter[0].style.transform = 'translate(0,' + scrollPos + 'px)'; 219 // } 220 // } 221 /** 222 * Fires when the off-canvas menu opens. 223 * @event OffCanvas#opened 224 */ 225 Foundation.Move(this.options.transitionTime, this.$element, function(){ 226 $('[data-off-canvas-wrapper]').addClass('is-off-canvas-open is-open-'+ _this.options.position); 227 228 _this.$element 229 .addClass('is-open') 230 231 // if(_this.options.isSticky){ 232 // _this._stick(); 233 // } 234 }); 235 this.$element.attr('aria-hidden', 'false') 236 .trigger('opened.zf.offcanvas'); 237 238 239 if(trigger){ 240 this.$lastTrigger = trigger.attr('aria-expanded', 'true'); 241 } 242 if(this.options.autoFocus){ 243 this.$element.one('finished.zf.animate', function(){ 244 _this.$element.find('a, button').eq(0).focus(); 245 }); 246 } 247 if(this.options.trapFocus){ 248 $('[data-off-canvas-content]').attr('tabindex', '-1'); 249 this._trapFocus(); 250 } 251}; 252/** 253 * Traps focus within the offcanvas on open. 254 * @private 255 */ 256OffCanvas.prototype._trapFocus = function(){ 257 var focusable = Foundation.Keyboard.findFocusable(this.$element), 258 first = focusable.eq(0), 259 last = focusable.eq(-1); 260 261 focusable.off('.zf.offcanvas').on('keydown.zf.offcanvas', function(e){ 262 if(e.which === 9 || e.keycode === 9){ 263 if(e.target === last[0] && !e.shiftKey){ 264 e.preventDefault(); 265 first.focus(); 266 } 267 if(e.target === first[0] && e.shiftKey){ 268 e.preventDefault(); 269 last.focus(); 270 } 271 } 272 }); 273}; 274/** 275 * Allows the offcanvas to appear sticky utilizing translate properties. 276 * @private 277 */ 278// OffCanvas.prototype._stick = function(){ 279// var elStyle = this.$element[0].style; 280// 281// if(this.options.closeOnClick){ 282// var exitStyle = this.$exiter[0].style; 283// } 284// 285// $(window).on('scroll.zf.offcanvas', function(e){ 286// console.log(e); 287// var pageY = window.pageYOffset; 288// elStyle.transform = 'translate(0,' + pageY + 'px)'; 289// if(exitStyle !== undefined){ exitStyle.transform = 'translate(0,' + pageY + 'px)'; } 290// }); 291// // this.$element.trigger('stuck.zf.offcanvas'); 292// }; 293/** 294 * Closes the off-canvas menu. 295 * @function 296 * @param {Function} cb - optional cb to fire after closure. 297 * @fires OffCanvas#closed 298 */ 299OffCanvas.prototype.close = function(cb) { 300 if(!this.$element.hasClass('is-open') || this.isRevealed){ return; } 301 302 var _this = this; 303 304 // Foundation.Move(this.options.transitionTime, this.$element, function(){ 305 $('[data-off-canvas-wrapper]').removeClass('is-off-canvas-open is-open-' + _this.options.position); 306 _this.$element.removeClass('is-open'); 307 // Foundation._reflow(); 308 // }); 309 this.$element.attr('aria-hidden', 'true') 310 /** 311 * Fires when the off-canvas menu opens. 312 * @event OffCanvas#closed 313 */ 314 .trigger('closed.zf.offcanvas'); 315 // if(_this.options.isSticky || !_this.options.forceTop){ 316 // setTimeout(function(){ 317 // _this.$element[0].style.transform = ''; 318 // $(window).off('scroll.zf.offcanvas'); 319 // }, this.options.transitionTime); 320 // } 321 322 this.$lastTrigger.attr('aria-expanded', 'false'); 323 if(this.options.trapFocus){ 324 $('[data-off-canvas-content]').removeAttr('tabindex'); 325 } 326 327}; 328 329/** 330 * Toggles the off-canvas menu open or closed. 331 * @function 332 * @param {Object} event - Event object passed from listener. 333 * @param {jQuery} trigger - element that triggered the off-canvas to open. 334 */ 335OffCanvas.prototype.toggle = function(event, trigger) { 336 if (this.$element.hasClass('is-open')) { 337 this.close(event, trigger); 338 } 339 else { 340 this.open(event, trigger); 341 } 342}; 343 344/** 345 * Handles keyboard input when detected. When the escape key is pressed, the off-canvas menu closes, and focus is restored to the element that opened the menu. 346 * @function 347 * @private 348 */ 349OffCanvas.prototype._handleKeyboard = function(event) { 350 if (event.which !== 27) return; 351 352 event.stopPropagation(); 353 event.preventDefault(); 354 this.close(); 355 this.$lastTrigger.focus(); 356}; 357/** 358 * Destroys the offcanvas plugin. 359 * @function 360 */ 361OffCanvas.prototype.destroy = function(){ 362 this.close(); 363 this.$element.off('.zf.trigger .zf.offcanvas'); 364 this.$exiter.off('.zf.offcanvas'); 365 366 Foundation.unregisterPlugin(this); 367}; 368 369Foundation.plugin(OffCanvas, 'OffCanvas'); 370 371}(jQuery, Foundation);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.