PageRenderTime 50ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/vendor/foundation-sites/js/foundation.offcanvas.js

https://gitlab.com/infogenix/foundation-wp
JavaScript | 434 lines | 224 code | 59 blank | 151 comment | 46 complexity | 78a47ba97dcad5bb43d7bdce98562847 MD5 | raw file
  1. 'use strict';
  2. !function($) {
  3. /**
  4. * OffCanvas module.
  5. * @module foundation.offcanvas
  6. * @requires foundation.util.keyboard
  7. * @requires foundation.util.mediaQuery
  8. * @requires foundation.util.triggers
  9. * @requires foundation.util.motion
  10. */
  11. class OffCanvas {
  12. /**
  13. * Creates a new instance of an off-canvas wrapper.
  14. * @class
  15. * @fires OffCanvas#init
  16. * @param {Object} element - jQuery object to initialize.
  17. * @param {Object} options - Overrides to the default plugin settings.
  18. */
  19. constructor(element, options) {
  20. this.$element = element;
  21. this.options = $.extend({}, OffCanvas.defaults, this.$element.data(), options);
  22. this.$lastTrigger = $();
  23. this.$triggers = $();
  24. this._init();
  25. this._events();
  26. Foundation.registerPlugin(this, 'OffCanvas')
  27. Foundation.Keyboard.register('OffCanvas', {
  28. 'ESCAPE': 'close'
  29. });
  30. }
  31. /**
  32. * Initializes the off-canvas wrapper by adding the exit overlay (if needed).
  33. * @function
  34. * @private
  35. */
  36. _init() {
  37. var id = this.$element.attr('id');
  38. this.$element.attr('aria-hidden', 'true');
  39. this.$element.addClass(`is-transition-${this.options.transition}`);
  40. // Find triggers that affect this element and add aria-expanded to them
  41. this.$triggers = $(document)
  42. .find('[data-open="'+id+'"], [data-close="'+id+'"], [data-toggle="'+id+'"]')
  43. .attr('aria-expanded', 'false')
  44. .attr('aria-controls', id);
  45. // Add an overlay over the content if necessary
  46. if (this.options.contentOverlay === true) {
  47. var overlay = document.createElement('div');
  48. var overlayPosition = $(this.$element).css("position") === 'fixed' ? 'is-overlay-fixed' : 'is-overlay-absolute';
  49. overlay.setAttribute('class', 'js-off-canvas-overlay ' + overlayPosition);
  50. this.$overlay = $(overlay);
  51. if(overlayPosition === 'is-overlay-fixed') {
  52. $('body').append(this.$overlay);
  53. } else {
  54. this.$element.siblings('[data-off-canvas-content]').append(this.$overlay);
  55. }
  56. }
  57. this.options.isRevealed = this.options.isRevealed || new RegExp(this.options.revealClass, 'g').test(this.$element[0].className);
  58. if (this.options.isRevealed === true) {
  59. this.options.revealOn = this.options.revealOn || this.$element[0].className.match(/(reveal-for-medium|reveal-for-large)/g)[0].split('-')[2];
  60. this._setMQChecker();
  61. }
  62. if (!this.options.transitionTime === true) {
  63. this.options.transitionTime = parseFloat(window.getComputedStyle($('[data-off-canvas]')[0]).transitionDuration) * 1000;
  64. }
  65. }
  66. /**
  67. * Adds event handlers to the off-canvas wrapper and the exit overlay.
  68. * @function
  69. * @private
  70. */
  71. _events() {
  72. this.$element.off('.zf.trigger .zf.offcanvas').on({
  73. 'open.zf.trigger': this.open.bind(this),
  74. 'close.zf.trigger': this.close.bind(this),
  75. 'toggle.zf.trigger': this.toggle.bind(this),
  76. 'keydown.zf.offcanvas': this._handleKeyboard.bind(this)
  77. });
  78. if (this.options.closeOnClick === true) {
  79. var $target = this.options.contentOverlay ? this.$overlay : $('[data-off-canvas-content]');
  80. $target.on({'click.zf.offcanvas': this.close.bind(this)});
  81. }
  82. }
  83. /**
  84. * Applies event listener for elements that will reveal at certain breakpoints.
  85. * @private
  86. */
  87. _setMQChecker() {
  88. var _this = this;
  89. $(window).on('changed.zf.mediaquery', function() {
  90. if (Foundation.MediaQuery.atLeast(_this.options.revealOn)) {
  91. _this.reveal(true);
  92. } else {
  93. _this.reveal(false);
  94. }
  95. }).one('load.zf.offcanvas', function() {
  96. if (Foundation.MediaQuery.atLeast(_this.options.revealOn)) {
  97. _this.reveal(true);
  98. }
  99. });
  100. }
  101. /**
  102. * Handles the revealing/hiding the off-canvas at breakpoints, not the same as open.
  103. * @param {Boolean} isRevealed - true if element should be revealed.
  104. * @function
  105. */
  106. reveal(isRevealed) {
  107. var $closer = this.$element.find('[data-close]');
  108. if (isRevealed) {
  109. this.close();
  110. this.isRevealed = true;
  111. this.$element.attr('aria-hidden', 'false');
  112. this.$element.off('open.zf.trigger toggle.zf.trigger');
  113. if ($closer.length) { $closer.hide(); }
  114. } else {
  115. this.isRevealed = false;
  116. this.$element.attr('aria-hidden', 'true');
  117. this.$element.off('open.zf.trigger toggle.zf.trigger').on({
  118. 'open.zf.trigger': this.open.bind(this),
  119. 'toggle.zf.trigger': this.toggle.bind(this)
  120. });
  121. if ($closer.length) {
  122. $closer.show();
  123. }
  124. }
  125. }
  126. /**
  127. * Stops scrolling of the body when offcanvas is open on mobile Safari and other troublesome browsers.
  128. * @private
  129. */
  130. _stopScrolling(event) {
  131. return false;
  132. }
  133. // Taken and adapted from http://stackoverflow.com/questions/16889447/prevent-full-page-scrolling-ios
  134. // Only really works for y, not sure how to extend to x or if we need to.
  135. _recordScrollable(event) {
  136. let elem = this; // called from event handler context with this as elem
  137. // If the element is scrollable (content overflows), then...
  138. if (elem.scrollHeight !== elem.clientHeight) {
  139. // If we're at the top, scroll down one pixel to allow scrolling up
  140. if (elem.scrollTop === 0) {
  141. elem.scrollTop = 1;
  142. }
  143. // If we're at the bottom, scroll up one pixel to allow scrolling down
  144. if (elem.scrollTop === elem.scrollHeight - elem.clientHeight) {
  145. elem.scrollTop = elem.scrollHeight - elem.clientHeight - 1;
  146. }
  147. }
  148. elem.allowUp = elem.scrollTop > 0;
  149. elem.allowDown = elem.scrollTop < (elem.scrollHeight - elem.clientHeight);
  150. elem.lastY = event.originalEvent.pageY;
  151. }
  152. _stopScrollPropagation(event) {
  153. let elem = this; // called from event handler context with this as elem
  154. let up = event.pageY < elem.lastY;
  155. let down = !up;
  156. elem.lastY = event.pageY;
  157. if((up && elem.allowUp) || (down && elem.allowDown)) {
  158. event.stopPropagation();
  159. } else {
  160. event.preventDefault();
  161. }
  162. }
  163. /**
  164. * Opens the off-canvas menu.
  165. * @function
  166. * @param {Object} event - Event object passed from listener.
  167. * @param {jQuery} trigger - element that triggered the off-canvas to open.
  168. * @fires OffCanvas#opened
  169. */
  170. open(event, trigger) {
  171. if (this.$element.hasClass('is-open') || this.isRevealed) { return; }
  172. var _this = this;
  173. if (trigger) {
  174. this.$lastTrigger = trigger;
  175. }
  176. if (this.options.forceTo === 'top') {
  177. window.scrollTo(0, 0);
  178. } else if (this.options.forceTo === 'bottom') {
  179. window.scrollTo(0,document.body.scrollHeight);
  180. }
  181. /**
  182. * Fires when the off-canvas menu opens.
  183. * @event OffCanvas#opened
  184. */
  185. _this.$element.addClass('is-open')
  186. this.$triggers.attr('aria-expanded', 'true');
  187. this.$element.attr('aria-hidden', 'false')
  188. .trigger('opened.zf.offcanvas');
  189. // If `contentScroll` is set to false, add class and disable scrolling on touch devices.
  190. if (this.options.contentScroll === false) {
  191. $('body').addClass('is-off-canvas-open').on('touchmove', this._stopScrolling);
  192. this.$element.on('touchstart', this._recordScrollable);
  193. this.$element.on('touchmove', this._stopScrollPropagation);
  194. }
  195. if (this.options.contentOverlay === true) {
  196. this.$overlay.addClass('is-visible');
  197. }
  198. if (this.options.closeOnClick === true && this.options.contentOverlay === true) {
  199. this.$overlay.addClass('is-closable');
  200. }
  201. if (this.options.autoFocus === true) {
  202. this.$element.one(Foundation.transitionend(this.$element), function() {
  203. var canvasFocus = _this.$element.find('[data-autofocus]');
  204. if (canvasFocus.length) {
  205. canvasFocus.eq(0).focus();
  206. } else {
  207. _this.$element.find('a, button').eq(0).focus();
  208. }
  209. });
  210. }
  211. if (this.options.trapFocus === true) {
  212. this.$element.siblings('[data-off-canvas-content]').attr('tabindex', '-1');
  213. Foundation.Keyboard.trapFocus(this.$element);
  214. }
  215. }
  216. /**
  217. * Closes the off-canvas menu.
  218. * @function
  219. * @param {Function} cb - optional cb to fire after closure.
  220. * @fires OffCanvas#closed
  221. */
  222. close(cb) {
  223. if (!this.$element.hasClass('is-open') || this.isRevealed) { return; }
  224. var _this = this;
  225. _this.$element.removeClass('is-open');
  226. this.$element.attr('aria-hidden', 'true')
  227. /**
  228. * Fires when the off-canvas menu opens.
  229. * @event OffCanvas#closed
  230. */
  231. .trigger('closed.zf.offcanvas');
  232. // If `contentScroll` is set to false, remove class and re-enable scrolling on touch devices.
  233. if (this.options.contentScroll === false) {
  234. $('body').removeClass('is-off-canvas-open').off('touchmove', this._stopScrolling);
  235. this.$element.off('touchstart', this._recordScrollable);
  236. this.$element.off('touchmove', this._stopScrollPropagation);
  237. }
  238. if (this.options.contentOverlay === true) {
  239. this.$overlay.removeClass('is-visible');
  240. }
  241. if (this.options.closeOnClick === true && this.options.contentOverlay === true) {
  242. this.$overlay.removeClass('is-closable');
  243. }
  244. this.$triggers.attr('aria-expanded', 'false');
  245. if (this.options.trapFocus === true) {
  246. this.$element.siblings('[data-off-canvas-content]').removeAttr('tabindex');
  247. Foundation.Keyboard.releaseFocus(this.$element);
  248. }
  249. }
  250. /**
  251. * Toggles the off-canvas menu open or closed.
  252. * @function
  253. * @param {Object} event - Event object passed from listener.
  254. * @param {jQuery} trigger - element that triggered the off-canvas to open.
  255. */
  256. toggle(event, trigger) {
  257. if (this.$element.hasClass('is-open')) {
  258. this.close(event, trigger);
  259. }
  260. else {
  261. this.open(event, trigger);
  262. }
  263. }
  264. /**
  265. * 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.
  266. * @function
  267. * @private
  268. */
  269. _handleKeyboard(e) {
  270. Foundation.Keyboard.handleKey(e, 'OffCanvas', {
  271. close: () => {
  272. this.close();
  273. this.$lastTrigger.focus();
  274. return true;
  275. },
  276. handled: () => {
  277. e.stopPropagation();
  278. e.preventDefault();
  279. }
  280. });
  281. }
  282. /**
  283. * Destroys the offcanvas plugin.
  284. * @function
  285. */
  286. destroy() {
  287. this.close();
  288. this.$element.off('.zf.trigger .zf.offcanvas');
  289. this.$overlay.off('.zf.offcanvas');
  290. Foundation.unregisterPlugin(this);
  291. }
  292. }
  293. OffCanvas.defaults = {
  294. /**
  295. * Allow the user to click outside of the menu to close it.
  296. * @option
  297. * @type {boolean}
  298. * @default true
  299. */
  300. closeOnClick: true,
  301. /**
  302. * Adds an overlay on top of `[data-off-canvas-content]`.
  303. * @option
  304. * @type {boolean}
  305. * @default true
  306. */
  307. contentOverlay: true,
  308. /**
  309. * Enable/disable scrolling of the main content when an off canvas panel is open.
  310. * @option
  311. * @type {boolean}
  312. * @default true
  313. */
  314. contentScroll: true,
  315. /**
  316. * Amount of time in ms the open and close transition requires. If none selected, pulls from body style.
  317. * @option
  318. * @type {number}
  319. * @default 0
  320. */
  321. transitionTime: 0,
  322. /**
  323. * Type of transition for the offcanvas menu. Options are 'push', 'detached' or 'slide'.
  324. * @option
  325. * @type {string}
  326. * @default push
  327. */
  328. transition: 'push',
  329. /**
  330. * Force the page to scroll to top or bottom on open.
  331. * @option
  332. * @type {?string}
  333. * @default null
  334. */
  335. forceTo: null,
  336. /**
  337. * Allow the offcanvas to remain open for certain breakpoints.
  338. * @option
  339. * @type {boolean}
  340. * @default false
  341. */
  342. isRevealed: false,
  343. /**
  344. * Breakpoint at which to reveal. JS will use a RegExp to target standard classes, if changing classnames, pass your class with the `revealClass` option.
  345. * @option
  346. * @type {?string}
  347. * @default null
  348. */
  349. revealOn: null,
  350. /**
  351. * Force focus to the offcanvas on open. If true, will focus the opening trigger on close.
  352. * @option
  353. * @type {boolean}
  354. * @default true
  355. */
  356. autoFocus: true,
  357. /**
  358. * Class used to force an offcanvas to remain open. Foundation defaults for this are `reveal-for-large` & `reveal-for-medium`.
  359. * @option
  360. * @type {string}
  361. * @default reveal-for-
  362. * @todo improve the regex testing for this.
  363. */
  364. revealClass: 'reveal-for-',
  365. /**
  366. * Triggers optional focus trapping when opening an offcanvas. Sets tabindex of [data-off-canvas-content] to -1 for accessibility purposes.
  367. * @option
  368. * @type {boolean}
  369. * @default false
  370. */
  371. trapFocus: false
  372. }
  373. // Window exports
  374. Foundation.plugin(OffCanvas, 'OffCanvas');
  375. }(jQuery);