Reusable Accessible Mapping Platform

API Docs for: 5.3.1
Show:

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

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