Reusable Accessible Mapping Platform

API Docs for: 3.0.0
Show:

File: src\js\RAMP\Utils\popupManager.js

  1. /*global define, window, $, document */
  2.  
  3. /**
  4. * Utility module containint useful static classes.
  5. *
  6. * @module Utils
  7. */
  8.  
  9. /**
  10. * A static class to simplify the creation of UI popups, where a popup is a section of the page hidden and shown in
  11. * response to some user or system action. This class takes care of assigning aria-* attributes and keeping them updated.
  12. *
  13. * @class PopupManager
  14. * @static
  15. * @uses dojo/Deferred
  16. * @uses dojo/_base/lang
  17. * @uses Util
  18. */
  19. define(["dojo/Deferred", "dojo/_base/lang", "utils/util"],
  20. function (Deferred, lang, UtilMisc) {
  21. "use strict";
  22.  
  23. /**
  24. * A class holding properties of the popup.
  25. *
  26. * @class PopupBaseSettings
  27. * @for PopupBase
  28. */
  29. var popupBaseAttrTemplate = {
  30. /**
  31. * The name of the event or events separated by a comma to trigger the closing of the popup.
  32. *
  33. * @property reverseEvent
  34. * @type {String}
  35. * @default null
  36. * @for PopupBaseSettings
  37. */
  38. reverseEvent: null,
  39.  
  40. /**
  41. * The initially supplied handle to the PopupManager; a {{#crossLink "jQuery"}}{{/crossLink}} to listen to events on.
  42. *
  43. * @property handle
  44. * @type {JQuery}
  45. * @default null
  46. * @for PopupBaseSettings
  47. */
  48. handle: null,
  49.  
  50. /**
  51. * The initially supplied handle selector to be used in conjunction with handle when listening to events. Useful if the real handle doesn't exist yet.
  52. *
  53. * @property handleSelector
  54. * @type {String}
  55. * @default null
  56. */
  57. handleSelector: null,
  58.  
  59. /**
  60. * The initially supplied target node of the popup.
  61. *
  62. * @property target
  63. * @type {JQuery}
  64. * @default null
  65. */
  66. target: null,
  67.  
  68. /**
  69. * The initially supplied target selector to be used in conjunction with target. Useful when the target of the popup doesn't exist yet.
  70. *
  71. * @property targetSelector
  72. * @type {String}
  73. * @default null
  74. */
  75. targetSelector: null,
  76.  
  77. targetContainerSelector: null,
  78.  
  79. /**
  80. * The function to execute when the popup opens.
  81. *
  82. * @property openHandler
  83. * @type {Function}
  84. * @default null
  85. */
  86. openHandler: null,
  87.  
  88. /**
  89. * The function to execute when the popup closes. If the function is not supplied, `openHandler` is used instead.
  90. *
  91. * @property closeHandler
  92. * @type {Function}
  93. * @default null
  94. */
  95. closeHandler: null,
  96.  
  97. /**
  98. * The delay before closing the popup; used with "hoverIntent" event type.
  99. *
  100. * @property timeout
  101. * @type {Number}
  102. * @default 0
  103. */
  104. timeout: 0,
  105.  
  106. /**
  107. * The CSS class to be applied to the handle of the popup when the popup opens.
  108. *
  109. * @property activeClass
  110. * @type {String}
  111. * @default random guid
  112. */
  113. activeClass: null,
  114.  
  115. /**
  116. * Indicates whether activeClass should be applied before openHandler function completes or after.
  117. *
  118. * @property setClassBefore
  119. * @type {String}
  120. * @default null
  121. */
  122. setClassBefore: false,
  123.  
  124. /**
  125. * Indicates whether to apply aria-* attributes to DOM nodes.
  126. *
  127. * @property useAria
  128. * @type {Boolean}
  129. * @default true
  130. */
  131. useAria: true,
  132.  
  133. /**
  134. * Indicates whether focus should be reset to the handle of the popup when the popup is closed by the internal close button if present.
  135. *
  136. * @property resetFocusOnClose
  137. * @type {Boolean}
  138. * @default false
  139. */
  140. resetFocusOnClose: false
  141. },
  142.  
  143. /**
  144. * An abstract representation of the popup definition that potentially references many Popup instances. Handle and target properties might use selectors.
  145. *
  146. * @class PopupBase
  147. * @for PopupManager
  148. */
  149. popupBaseTemplate = {
  150. /**
  151. * Properties object of the PopupBase.
  152. *
  153. * @property _attr
  154. * @private
  155. * @type {PopupBaseSettings}
  156. * @for PopupBase
  157. */
  158. _attr: null,
  159.  
  160. /**
  161. * Finds and returns actual DOM nodes of popup handles, one or more. Used selector
  162. *
  163. * @method _getActualHandle
  164. * @private
  165. * @param {JQuery} [selector] A {{#crossLink "jQuery"}}{{/crossLink}} of the actual handle.
  166. * @return result An array of one or more jQuery objects that works as popup handles
  167. */
  168. _getActualHandle: function (selector) {
  169. var result;
  170.  
  171. if (selector) {
  172. result = $(selector);
  173. } else if (this._attr.handle) {
  174. result = this._attr.handleSelector ? this._attr.handle.find(this._attr.handleSelector) : this._attr.handle;
  175. }
  176.  
  177. return result;
  178. },
  179.  
  180. /**
  181. * Finds and returns an array of {{#crossLink "Popup"}}{{/crossLink}} objects, one or more, identified in the PopupBase.
  182. *
  183. * @method _spawnPopups
  184. * @private
  185. * @param {JQuery} [selector] A {{#crossLink "jQuery"}}{{/crossLink}} of the actual handle.
  186. * @return popups An array of one or more {{#crossLink "Popup"}}{{/crossLink}} objects
  187. */
  188. _spawnPopups: function (selector) {
  189. var popups = [],
  190. actualHandle = this._getActualHandle(selector),
  191. actualTarget;
  192.  
  193. actualHandle.each(lang.hitch(this,
  194. function (i, ah) {
  195. ah = $(ah);
  196.  
  197. if (this._attr.target) {
  198. actualTarget = this._attr.targetSelector ? this._attr.target.find(this._attr.targetSelector) : this._attr.target;
  199. } else if (this._attr.targetContainerSelector && this._attr.targetSelector) {
  200. actualTarget = ah.parents(this._attr.targetContainerSelector).find(this._attr.targetSelector);
  201. } else {
  202. // if the target cannot be found, a handle its returned
  203. actualTarget = this._attr.targetSelector ? ah.find(this._attr.targetSelector) : ah;
  204. }
  205.  
  206. if (actualTarget.length > 0) {
  207. popups.push(
  208. lang.mixin(Object.create(popupTempate), {
  209. openHandler: this._attr.openHandler,
  210. closeHandler: this._attr.closeHandler || this._attr.openHandler,
  211.  
  212. activeClass: this._attr.activeClass,
  213. setClassBefore: this._attr.setClassBefore,
  214. useAria: this._attr.useAria,
  215. resetFocusOnClose: this._attr.resetFocusOnClose,
  216.  
  217. handle: actualHandle,
  218. target: actualTarget
  219. })
  220. );
  221. }
  222. }));
  223.  
  224. return popups;
  225. },
  226.  
  227. /**
  228. * Checks if any of the popups described by this PopupBase is closed.
  229. *
  230. * @method isOpen
  231. * @param {JQuery} [selector] A {{#crossLink "jQuery"}}{{/crossLink}} of the actual handle.
  232. * @return result True if any of the described popup are open; false otherwise
  233. */
  234. isOpen: function (selector) {
  235. var result = true;
  236.  
  237. this._spawnPopups(selector).forEach(function (p) {
  238. if (!p.isOpen()) {
  239. result = false;
  240. }
  241. });
  242.  
  243. return result;
  244. },
  245.  
  246. /**
  247. * Opens all the popups described by this PopupBase instance.
  248. *
  249. * @method open
  250. * @param {JQuery} [selector] A {{#crossLink "jQuery"}}{{/crossLink}} of the actual handle.
  251. */
  252. open: function (selector) {
  253. this._spawnPopups(selector).forEach(function (p) {
  254. p.open();
  255. });
  256. },
  257.  
  258. /**
  259. * Closes all the popups described by this PopupBase instance.
  260. *
  261. * @method close
  262. * @param {JQuery} [selector] A {{#crossLink "jQuery"}}{{/crossLink}} of the actual handle.
  263. */
  264. close: function (selector) {
  265. this._spawnPopups(selector).forEach(function (p) {
  266. p.close();
  267. });
  268. },
  269.  
  270. /**
  271. * Toggles all the popups described by this PopupBase instance.
  272. *
  273. * @method toggle
  274. * @param {JQuery} [selector] A {{#crossLink "jQuery"}}{{/crossLink}} of the actual handle. Generally, selector is not needed if popup manages only one handle/target pair.
  275. * @param {Boolean} [state] Indicates if the popup should be toggled on or off. true - open; false - close;
  276. */
  277. toggle: function (selector, state) {
  278. this._spawnPopups(selector).forEach(function (p) {
  279. p.toggle(state);
  280. });
  281. },
  282.  
  283. /**
  284. * Sets the appropriate aria-* attributes to the popup nodes according to the supplied `visible` parameter or with the internal state of the popup.
  285. *
  286. * @method setTargetAttr
  287. * @param {Boolean} [visible] Indicating the internal state of the popup
  288. */
  289. setTargetAttr: function (visible) {
  290. this._spawnPopups().forEach(function (p) {
  291. p.setTargetAttr(visible);
  292. });
  293. }
  294. },
  295.  
  296. /**
  297. * A concrete instance of popup referencing actual DOM nodes as its handle and target.
  298. *
  299. * @class Popup
  300. * @for PopupManager
  301. */
  302. popupTempate = {
  303. /**
  304. * Indicates if the Popup target is being animated.
  305. *
  306. * @property _isAnimating
  307. * @type {Boolean}
  308. * @for Popup
  309. * @private
  310. */
  311. _isAnimating: false,
  312.  
  313. /**
  314. * The function to execute when the popup opens.
  315. *
  316. * @property openHandler
  317. * @type {Function}
  318. * @default null
  319. */
  320. openHandler: null,
  321.  
  322. /**
  323. * The function to execute when the popup closes.
  324. *
  325. * @property closeHandler
  326. * @type {Function}
  327. * @default null
  328. */
  329. closeHandler: null,
  330.  
  331. /**
  332. * The CSS class to be applied to the handle of the popup when the popup opens.
  333. *
  334. * @property activeClass
  335. * @type {String}
  336. * @default null
  337. */
  338. activeClass: null,
  339.  
  340. /**
  341. * Indicates whether activeClass should be applied before openHandler function completes or after.
  342. *
  343. * @property setClassBefore
  344. * @type {String}
  345. * @default null
  346. */
  347. setClassBefore: null,
  348.  
  349. /**
  350. * Indicates whether to apply aria-* attributes to DOM nodes.
  351. *
  352. * @property useAria
  353. * @type {Boolean}
  354. * @default true
  355. */
  356. useAria: null,
  357.  
  358. /**
  359. * An actual {{#crossLink "jQuery"}}{{/crossLink}} of the handle's DOM node.
  360. *
  361. * @property handle
  362. * @type {JQuery}
  363. * @default null
  364. */
  365. handle: null,
  366.  
  367. /**
  368. * An actual {{#crossLink "jQuery"}}{{/crossLink}} of the targets's DOM node.
  369. *
  370. * @property target
  371. * @type {JQuery}
  372. * @default null
  373. */
  374. target: null,
  375.  
  376. /**
  377. * Checks if this Popup is open.
  378. *
  379. * @method isOpen
  380. * @return {Boolean} True if open, false otherwise
  381. */
  382. isOpen: function () {
  383. return this.handle.hasClass(this.activeClass);
  384. },
  385.  
  386. /**
  387. * Opens this Popup.
  388. *
  389. * @method open
  390. */
  391. open: function () {
  392. this._performAction(
  393. this.openHandler,
  394.  
  395. function () {
  396. this.handle.addClass(this.activeClass);
  397. },
  398.  
  399. function () {
  400. this.setTargetAttr(true);
  401. }
  402. );
  403. },
  404.  
  405. /**
  406. * Closes this Popup.
  407. *
  408. * @method close
  409. */
  410. close: function () {
  411. this._performAction(
  412. this.closeHandler,
  413.  
  414. function () {
  415. this.handle.removeClass(this.activeClass);
  416. },
  417.  
  418. function () {
  419. this.setTargetAttr(false);
  420. }
  421. );
  422. },
  423.  
  424. /**
  425. * Toggles this Popup.
  426. *
  427. * @method toggle
  428. * @param {Boolean} [state] Indicates if the popup should be toggled on or off. true - open; false - close;
  429. */
  430. toggle: function (state) {
  431. state = UtilMisc.isUndefined(state) ? this.isOpen() : !state;
  432. if (state) {
  433. this.close();
  434. } else {
  435. this.open();
  436. }
  437. },
  438.  
  439. /**
  440. * Performs actions like closing and opening on this Popup.
  441. *
  442. * @method _performAction
  443. * @private
  444. * @param {Function} action Open or close action on this Popup
  445. * @param {Function} cssAction Function setting style properties on this Popup
  446. * @param {Function} callback The callback to be executed
  447. */
  448. _performAction: function (action, cssAction, callback) {
  449. if ($.isFunction(action) && !this._isAnimating) {
  450. var deferred = new Deferred();
  451.  
  452. deferred.then(lang.hitch(this,
  453. function () {
  454. this._isAnimating = false;
  455.  
  456. if (!this.setClassBefore) {
  457. cssAction.call(this);
  458. }
  459.  
  460. callback.call(this);
  461. }));
  462.  
  463. this._isAnimating = true;
  464.  
  465. if (this.setClassBefore) {
  466. cssAction.call(this);
  467. }
  468.  
  469. action.call(this, deferred);
  470. }
  471. },
  472.  
  473. /**
  474. * Sets the appropriate aria-* attributes to this popup nodes according to the supplied `visible` parameter or with the internal state of the popup.
  475. *
  476. * @method setTargetAttr
  477. * @param {Boolean} [visible] Indicating the internal state of the popup
  478. */
  479. setTargetAttr: function (visible) {
  480. if (visible !== true && visible !== false) {
  481. visible = this.isOpen();
  482. }
  483.  
  484. if (this.useAria) {
  485. this.handle.attr("aria-pressed", visible);
  486.  
  487. // if handle and target are the same object, do not set aria attributes on target
  488. if (this.handle[0] !== this.target[0]) {
  489. this.target.attr({
  490. "aria-expanded": visible,
  491. "aria-hidden": !visible
  492. });
  493. }
  494. }
  495. }
  496. };
  497.  
  498. /**
  499. * Create a new PopupBase object from the settings provided.
  500. *
  501. * @method newPopup
  502. * @private
  503. * @param {PopupBaseSettings} popupAttr Popup settings
  504. * @return popup
  505. * @for PopupManager
  506. */
  507. function newPopup(popupAttr) {
  508. var popup = Object.create(popupBaseTemplate, {
  509. _attr: {
  510. value: popupAttr
  511. }
  512. });
  513.  
  514. popup._spawnPopups().forEach(function (p) {
  515. if (p.useAria) {
  516. p.handle.attr("aria-pressed", false);
  517.  
  518. // if handle and target are the same object, do not set aria attributes on target
  519. if (p.handle[0] !== p.target[0]) {
  520. p.handle.attr("aria-haspopup", true);
  521. }
  522.  
  523. p.setTargetAttr();
  524. }
  525.  
  526. p.target.find(".button-close").on("click",
  527. function () {
  528. p.close();
  529.  
  530. // reset the focus to the popup's handle when the popup's internal close button is clicked
  531. if (p.resetFocusOnClose) {
  532. p.handle.focus();
  533. }
  534. });
  535. });
  536.  
  537. return popup;
  538. }
  539.  
  540. return {
  541. /**
  542. * Register a PopupBase definition. By a popup here we mean a section of the page that reacts to the user's action on this or different section of the page.
  543. * Can be used to register popups with already existing page nodes, or, using handle and target selectors with the nodes that will be created later.
  544. *
  545. * ####Example
  546. * popupManager.registerPopup(panelToggle, "click",
  547. * openPanel,
  548. * {
  549. * activeClass: cssExpandedClass,
  550. * closeHandler: closePanel
  551. * }
  552. * );
  553. * Here we register a popup on the `panelToggle` node which will trigger `openPanel` function when the user clicks to open the popup and `closePanel` to close it;
  554. * `cssExpandedClass` will be set on the `panelToggle` node when the popup is opened.
  555. *
  556. * popupManager.registerPopup(sectionNode, "hover, focus",
  557. * openFunction,
  558. * {
  559. * handleSelector: "tr",
  560. *
  561. * targetSelector: ".record-controls",
  562. *
  563. * closeHandler: closeFunction,
  564. *
  565. * activeClass: "bg-very-light",
  566. * useAria: false
  567. * }
  568. * );
  569. * Here we define a set of virtual popups on the `sectionNode` node that would be triggered when the user hovers over or sets focus to any `tr` child node of `sectionNode`.
  570. * Then the `openFunction` will be executed with `this.handle` pointing to the actual handle node which trigged the popup and `this.target` pointing to the actual target node
  571. * corresponding to a node or nodes found with the `targetSelector` inside the actual handle node.
  572. *
  573. * @method registerPopup
  574. * @static
  575. * @param {jQuery} handle A {{#crossLink "jQuery"}}{{/crossLink}} handle to listen to events on
  576. * @param {String} event The name of the event or events separated by a comma to trigger the popup. There are several predefined event names to register hover popups:
  577. * - `hoverIntent` uses the hoverIntent jQuery plugin to determine when the user intends to hover over something
  578. * - `hover` is a combination of two events - `mouseleave` and `mouseenter` and unlike `hoverIntent` it is triggered immediatelly
  579. * - `focus` is a combination of two events - `focusin` and `focusout`
  580. * You can subscribe to a combination of event shortcuts like `focus,hover`
  581. *
  582. * Additionally, almost any other {{#crossLink "jQuery"}}{{/crossLink}} event can be specified like `click` or `keypress`.
  583. * @param {Function} openHandler The function to run when the popup opens
  584. * @param {PopupBaseSettings} [settings] additional setting to define the popup
  585. * @return {PopupBase} Returns a PopupBase with the specified conditions
  586. */
  587. registerPopup: function (handle, event, openHandler, settings) {
  588. var popup,
  589. popupAttr;
  590.  
  591. // splitting event names
  592. event = event.split(",").map(function (a) {
  593. return a.trim();
  594. });
  595.  
  596. // mixing default and user-provided settings
  597. popupAttr = lang.mixin(Object.create(popupBaseAttrTemplate),
  598. {
  599. activeClass: UtilMisc.guid()
  600. },
  601. settings,
  602. {
  603. handle: handle,
  604. openHandler: openHandler
  605. }
  606. );
  607.  
  608. popup = newPopup(popupAttr);
  609.  
  610. // iterating over event array
  611. event.forEach(function (e) {
  612. switch (e) {
  613. // hover intent uses a jQuery plugin: http://cherne.net/brian/resources/jquery.hoverIntent.html
  614. // this plugin is loaded by WET, and sometimes it might not be loaded fast enough, so we use executeOnLoad to wait for the plugin to load
  615. case "hoverIntent":
  616. var timeoutHandle,
  617. open = function (event) {
  618. window.clearTimeout(timeoutHandle);
  619. popup.open(event.currentTarget);
  620. },
  621. close = function (event) {
  622. var t = event ? event.currentTarget : null;
  623. popup.close(t);
  624. };
  625.  
  626. UtilMisc.executeOnLoad($(document), "hoverIntent", function () {
  627. popup._attr.handle
  628. .hoverIntent({
  629. over: open,
  630. out: close,
  631. selector: popup._attr.handleSelector,
  632. timeout: popup._attr.timeout
  633. })
  634. .on("click focusin", popup._attr.handleSelector, open)
  635. .on("focusout", popup._attr.handleSelector, function () {
  636. timeoutHandle = window.setTimeout(close, popup._attr.timeout);
  637. });
  638. });
  639.  
  640. break;
  641.  
  642. case "hover":
  643. popup._attr.handle
  644. .on("mouseenter", popup._attr.handleSelector,
  645. function (event) {
  646. popup.open(event.currentTarget);
  647. })
  648. .on("mouseleave", popup._attr.handleSelector,
  649. function (event) {
  650. popup.close(event.currentTarget);
  651. });
  652. break;
  653.  
  654. case "focus":
  655. popup._attr.handle
  656. .on("focusin", popup._attr.handleSelector,
  657. function (event) {
  658. popup.open(event.currentTarget);
  659. })
  660. .on("focusout", popup._attr.handleSelector,
  661. function (event) {
  662. popup.close(event.currentTarget);
  663. });
  664.  
  665. break;
  666.  
  667. default:
  668. if (popup._attr.reverseEvent) {
  669. handle
  670. .on(e, popup._attr.handleSelector, function (event) {
  671. popup.open(event.currentTarget);
  672. })
  673. .on(popup._attr.reverseEvent, popup._attr.handleSelector, function (event) {
  674. popup.close(event.currentTarget);
  675. });
  676. } else {
  677. handle.on(e, popup._attr.handleSelector, function (event) {
  678. popup.toggle(event.currentTarget);
  679. });
  680. }
  681.  
  682. break;
  683. }
  684. });
  685.  
  686. return popup;
  687. }
  688. };
  689. });