Reusable Accessible Mapping Platform

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