off-canvas.es6.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. /**
  2. * @file
  3. * Drupal's off-canvas library.
  4. */
  5. (($, Drupal, debounce, displace) => {
  6. /**
  7. * Off-canvas dialog implementation using jQuery Dialog.
  8. *
  9. * Transforms the regular dialogs created using Drupal.dialog when the dialog
  10. * element equals '#drupal-off-canvas' into an side-loading dialog.
  11. *
  12. * @namespace
  13. */
  14. Drupal.offCanvas = {
  15. /**
  16. * Storage for position information about the tray.
  17. *
  18. * @type {?String}
  19. */
  20. position: null,
  21. /**
  22. * The minimum height of the tray when opened at the top of the page.
  23. *
  24. * @type {Number}
  25. */
  26. minimumHeight: 30,
  27. /**
  28. * The minimum width to use body displace needs to match the width at which
  29. * the tray will be 100% width. @see core/misc/dialog/off-canvas.css
  30. *
  31. * @type {Number}
  32. */
  33. minDisplaceWidth: 768,
  34. /**
  35. * Wrapper used to position off-canvas dialog.
  36. *
  37. * @type {jQuery}
  38. */
  39. $mainCanvasWrapper: $('[data-off-canvas-main-canvas]'),
  40. /**
  41. * Determines if an element is an off-canvas dialog.
  42. *
  43. * @param {jQuery} $element
  44. * The dialog element.
  45. *
  46. * @return {bool}
  47. * True this is currently an off-canvas dialog.
  48. */
  49. isOffCanvas($element) {
  50. return $element.is('#drupal-off-canvas');
  51. },
  52. /**
  53. * Remove off-canvas dialog events.
  54. *
  55. * @param {jQuery} $element
  56. * The target element.
  57. */
  58. removeOffCanvasEvents($element) {
  59. $element.off('.off-canvas');
  60. $(document).off('.off-canvas');
  61. $(window).off('.off-canvas');
  62. },
  63. /**
  64. * Handler fired before an off-canvas dialog has been opened.
  65. *
  66. * @param {Object} settings
  67. * Settings related to the composition of the dialog.
  68. *
  69. * @return {undefined}
  70. */
  71. beforeCreate({ settings, $element }) {
  72. // Clean up previous dialog event handlers.
  73. Drupal.offCanvas.removeOffCanvasEvents($element);
  74. $('body').addClass('js-off-canvas-dialog-open');
  75. // @see http://api.jqueryui.com/position/
  76. settings.position = {
  77. my: 'left top',
  78. at: `${Drupal.offCanvas.getEdge()} top`,
  79. of: window,
  80. };
  81. /**
  82. * Applies initial height and with to dialog based depending on position.
  83. * @see http://api.jqueryui.com/dialog for all dialog options.
  84. */
  85. const position = settings.drupalOffCanvasPosition;
  86. const height = position === 'side' ? $(window).height() : settings.height;
  87. const width = position === 'side' ? settings.width : '100%';
  88. settings.height = height;
  89. settings.width = width;
  90. },
  91. /**
  92. * Handler fired after an off-canvas dialog has been closed.
  93. *
  94. * @return {undefined}
  95. */
  96. beforeClose({ $element }) {
  97. $('body').removeClass('js-off-canvas-dialog-open');
  98. // Remove all *.off-canvas events
  99. Drupal.offCanvas.removeOffCanvasEvents($element);
  100. Drupal.offCanvas.resetPadding();
  101. },
  102. /**
  103. * Handler fired when an off-canvas dialog has been opened.
  104. *
  105. * @param {jQuery} $element
  106. * The off-canvas dialog element.
  107. * @param {Object} settings
  108. * Settings related to the composition of the dialog.
  109. *
  110. * @return {undefined}
  111. */
  112. afterCreate({ $element, settings }) {
  113. const eventData = { settings, $element, offCanvasDialog: this };
  114. $element
  115. .on(
  116. 'dialogContentResize.off-canvas',
  117. eventData,
  118. Drupal.offCanvas.handleDialogResize,
  119. )
  120. .on(
  121. 'dialogContentResize.off-canvas',
  122. eventData,
  123. Drupal.offCanvas.bodyPadding,
  124. );
  125. Drupal.offCanvas
  126. .getContainer($element)
  127. .attr(`data-offset-${Drupal.offCanvas.getEdge()}`, '');
  128. $(window)
  129. .on(
  130. 'resize.off-canvas',
  131. eventData,
  132. debounce(Drupal.offCanvas.resetSize, 100),
  133. )
  134. .trigger('resize.off-canvas');
  135. },
  136. /**
  137. * Toggle classes based on title existence.
  138. * Called with Drupal.offCanvas.afterCreate.
  139. *
  140. * @param {Object} settings
  141. * Settings related to the composition of the dialog.
  142. *
  143. * @return {undefined}
  144. */
  145. render({ settings }) {
  146. $(
  147. '.ui-dialog-off-canvas, .ui-dialog-off-canvas .ui-dialog-titlebar',
  148. ).toggleClass('ui-dialog-empty-title', !settings.title);
  149. },
  150. /**
  151. * Adjusts the dialog on resize.
  152. *
  153. * @param {jQuery.Event} event
  154. * The event triggered.
  155. * @param {object} event.data
  156. * Data attached to the event.
  157. */
  158. handleDialogResize(event) {
  159. const $element = event.data.$element;
  160. const $container = Drupal.offCanvas.getContainer($element);
  161. const $offsets = $container.find(
  162. '> :not(#drupal-off-canvas, .ui-resizable-handle)',
  163. );
  164. let offset = 0;
  165. // Let scroll element take all the height available.
  166. $element.css({ height: 'auto' });
  167. const modalHeight = $container.height();
  168. $offsets.each((i, e) => {
  169. offset += $(e).outerHeight();
  170. });
  171. // Take internal padding into account.
  172. const scrollOffset = $element.outerHeight() - $element.height();
  173. $element.height(modalHeight - offset - scrollOffset);
  174. },
  175. /**
  176. * Resets the size of the dialog.
  177. *
  178. * @param {jQuery.Event} event
  179. * The event triggered.
  180. * @param {object} event.data
  181. * Data attached to the event.
  182. */
  183. resetSize(event) {
  184. const $element = event.data.$element;
  185. const container = Drupal.offCanvas.getContainer($element);
  186. const position = event.data.settings.drupalOffCanvasPosition;
  187. // Only remove the `data-offset-*` attribute if the value previously
  188. // exists and the orientation is changing.
  189. if (Drupal.offCanvas.position && Drupal.offCanvas.position !== position) {
  190. container.removeAttr(`data-offset-${Drupal.offCanvas.position}`);
  191. }
  192. // Set a minimum height on $element
  193. if (position === 'top') {
  194. $element.css('min-height', `${Drupal.offCanvas.minimumHeight}px`);
  195. }
  196. displace();
  197. const offsets = displace.offsets;
  198. const topPosition =
  199. position === 'side' && offsets.top !== 0 ? `+${offsets.top}` : '';
  200. const adjustedOptions = {
  201. // @see http://api.jqueryui.com/position/
  202. position: {
  203. my: `${Drupal.offCanvas.getEdge()} top`,
  204. at: `${Drupal.offCanvas.getEdge()} top${topPosition}`,
  205. of: window,
  206. },
  207. };
  208. const height =
  209. position === 'side'
  210. ? `${$(window).height() - (offsets.top + offsets.bottom)}px`
  211. : event.data.settings.height;
  212. container.css({
  213. position: 'fixed',
  214. height,
  215. });
  216. $element
  217. .dialog('option', adjustedOptions)
  218. .trigger('dialogContentResize.off-canvas');
  219. Drupal.offCanvas.position = position;
  220. },
  221. /**
  222. * Adjusts the body padding when the dialog is resized.
  223. *
  224. * @param {jQuery.Event} event
  225. * The event triggered.
  226. * @param {object} event.data
  227. * Data attached to the event.
  228. */
  229. bodyPadding(event) {
  230. const position = event.data.settings.drupalOffCanvasPosition;
  231. if (
  232. position === 'side' &&
  233. $('body').outerWidth() < Drupal.offCanvas.minDisplaceWidth
  234. ) {
  235. return;
  236. }
  237. Drupal.offCanvas.resetPadding();
  238. const $element = event.data.$element;
  239. const $container = Drupal.offCanvas.getContainer($element);
  240. const $mainCanvasWrapper = Drupal.offCanvas.$mainCanvasWrapper;
  241. const width = $container.outerWidth();
  242. const mainCanvasPadding = $mainCanvasWrapper.css(
  243. `padding-${Drupal.offCanvas.getEdge()}`,
  244. );
  245. if (position === 'side' && width !== mainCanvasPadding) {
  246. $mainCanvasWrapper.css(
  247. `padding-${Drupal.offCanvas.getEdge()}`,
  248. `${width}px`,
  249. );
  250. $container.attr(`data-offset-${Drupal.offCanvas.getEdge()}`, width);
  251. displace();
  252. }
  253. const height = $container.outerHeight();
  254. if (position === 'top') {
  255. $mainCanvasWrapper.css('padding-top', `${height}px`);
  256. $container.attr('data-offset-top', height);
  257. displace();
  258. }
  259. },
  260. /**
  261. * The HTML element that surrounds the dialog.
  262. * @param {HTMLElement} $element
  263. * The dialog element.
  264. *
  265. * @return {HTMLElement}
  266. * The containing element.
  267. */
  268. getContainer($element) {
  269. return $element.dialog('widget');
  270. },
  271. /**
  272. * The edge of the screen that the dialog should appear on.
  273. *
  274. * @return {string}
  275. * The edge the tray will be shown on, left or right.
  276. */
  277. getEdge() {
  278. return document.documentElement.dir === 'rtl' ? 'left' : 'right';
  279. },
  280. /**
  281. * Resets main canvas wrapper and toolbar padding / margin.
  282. */
  283. resetPadding() {
  284. Drupal.offCanvas.$mainCanvasWrapper.css(
  285. `padding-${Drupal.offCanvas.getEdge()}`,
  286. 0,
  287. );
  288. Drupal.offCanvas.$mainCanvasWrapper.css('padding-top', 0);
  289. displace();
  290. },
  291. };
  292. /**
  293. * Attaches off-canvas dialog behaviors.
  294. *
  295. * @type {Drupal~behavior}
  296. *
  297. * @prop {Drupal~behaviorAttach} attach
  298. * Attaches event listeners for off-canvas dialogs.
  299. */
  300. Drupal.behaviors.offCanvasEvents = {
  301. attach: () => {
  302. $(window)
  303. .once('off-canvas')
  304. .on({
  305. 'dialog:beforecreate': (event, dialog, $element, settings) => {
  306. if (Drupal.offCanvas.isOffCanvas($element)) {
  307. Drupal.offCanvas.beforeCreate({ dialog, $element, settings });
  308. }
  309. },
  310. 'dialog:aftercreate': (event, dialog, $element, settings) => {
  311. if (Drupal.offCanvas.isOffCanvas($element)) {
  312. Drupal.offCanvas.render({ dialog, $element, settings });
  313. Drupal.offCanvas.afterCreate({ $element, settings });
  314. }
  315. },
  316. 'dialog:beforeclose': (event, dialog, $element) => {
  317. if (Drupal.offCanvas.isOffCanvas($element)) {
  318. Drupal.offCanvas.beforeClose({ dialog, $element });
  319. }
  320. },
  321. });
  322. },
  323. };
  324. })(jQuery, Drupal, Drupal.debounce, Drupal.displace);