menu-loader.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. define(["options", "jquery", "nav"], function (options, $, navConfig) {
  2. /**
  3. * The path of the output directory, relative to the current HTML file.
  4. * @type {String}
  5. */
  6. var path2root = null;
  7. // Register the hover handler for the Menu
  8. $(document).ready ( function() {
  9. // Add mouse listener and set the wai-aria attributes to the new value, depending on the expanded/collapsed state
  10. $(document).on("mouseenter", ".wh_top_menu li", menuItemHovered);
  11. // Mouse exit
  12. $(document).on("mouseleave", ".wh_top_menu li", function () {
  13. var stateNode = $(this).children(".topicref");
  14. var currentState = stateNode.attr(navConfig.attrs.state);
  15. if (currentState !== navConfig.states.leaf) {
  16. $(this).attr("aria-expanded", "false");
  17. stateNode.attr(navConfig.attrs.state, navConfig.states.collapsed);
  18. }
  19. });
  20. });
  21. /**
  22. * Retrieves the path of the output directory, relative to the current HTML file.
  23. *
  24. * @returns {*} the path of the output directory, relative to the current HTML file.
  25. */
  26. function getPathToRoot() {
  27. if (path2root == null) {
  28. path2root = $('meta[name="wh-path2root"]').attr("content");
  29. if (path2root == null || path2root == undefined) {
  30. path2root = "";
  31. }
  32. }
  33. return path2root;
  34. };
  35. /**
  36. * Loads the JS file containing the list of child nodes for the current topic node.
  37. * Builds the list of child topics element nodes based on the retrieved data.
  38. *
  39. * @param topicRefSpan The topicref 'span' element of the current node from TOC / Menu.
  40. */
  41. function retrieveChildNodes(topicRefSpan) {
  42. var tocId = $(topicRefSpan).attr(navConfig.attrs.tocID);
  43. if (tocId != null) {
  44. var jsonHref = navConfig.jsonBaseDir + "/" + tocId;
  45. require(
  46. [jsonHref],
  47. function(data) {
  48. if (data != null) {
  49. var topics = data.topics;
  50. var topicLi = topicRefSpan.closest('li');
  51. var topicsLiList = createTopicsList(topics);
  52. var loadingDotsUl = topicLi.children("ul.loading");
  53. // Remove the loading dots from the menu.
  54. loadingDotsUl.find('li').remove();
  55. loadingDotsUl.removeClass('loading');
  56. topicsLiList.forEach(function(topicLi){
  57. loadingDotsUl.append(topicLi);
  58. });
  59. topicRefSpan.attr(navConfig.attrs.state, navConfig.states.expanded);
  60. } else {
  61. topicRefSpan.attr(navConfig.attrs.state, navConfig.states.leaf);
  62. }
  63. }
  64. );
  65. }
  66. }
  67. /**
  68. * Creates the <code>ul</code> element containing the child topic nodes of the current topic.
  69. *
  70. * @param topics The array of containing info about the child topics.
  71. *
  72. * @returns {*|jQuery|HTMLElement} the <code>li</code> elements representing the child topic nodes of the current topic.
  73. */
  74. function createTopicsList(topics) {
  75. var topicsArray = [];
  76. topics.forEach(function(topic) {
  77. if (!topic.menu.isHidden) {
  78. var topicLi = createTopicLi(topic);
  79. topicsArray.push(topicLi);
  80. }
  81. });
  82. return topicsArray;
  83. };
  84. /**
  85. * Creates the <code>li</code> element containing a topic node.
  86. *
  87. * @param topic The info about the topic node.
  88. *
  89. * @returns {*|jQuery|HTMLElement} the <code>li</code> element containing a topic node.
  90. */
  91. function createTopicLi(topic) {
  92. var li = $("<li role=\"menuitem\">");
  93. if (topic.menu.hasChildren) {
  94. li.addClass("has-children");
  95. li.attr("aria-haspopup", "true");
  96. li.attr("aria-expanded", "false");
  97. }
  98. var topicImage = topic.menu.image;
  99. if (topicImage != null && topicImage.href != null) {
  100. var menuImageSpan = createMenuImageSpan(topic);
  101. li.append(menuImageSpan);
  102. }
  103. // .topicref span
  104. var topicRefSpan = createTopicRefSpan(topic);
  105. // append the topicref node in parent
  106. li.append(topicRefSpan);
  107. return li;
  108. };
  109. /**
  110. * Creates the <span> element containing the image icon for the current node in the menu.
  111. * @param topic The JSON object containing the info about the associated node.
  112. *
  113. * @returns {*|jQuery|HTMLElement} the image 'span' element.
  114. */
  115. function createMenuImageSpan(topic) {
  116. var topicImage = topic.menu.image;
  117. // Image span
  118. var imgSpan = $("<span>", {class : "topicImg"});
  119. var isExternalReference = topicImage.scope == 'external';
  120. var imageHref = '';
  121. if (!isExternalReference) {
  122. imageHref += getPathToRoot();
  123. }
  124. imageHref += topicImage.href;
  125. var img = $("<img>", {
  126. src : imageHref,
  127. alt : topic.title
  128. });
  129. if (topicImage.height != null) {
  130. img.attr("height", topicImage.height);
  131. }
  132. if (topicImage.width != null) {
  133. img.attr("width", topicImage.width);
  134. }
  135. imgSpan.append(img);
  136. return imgSpan;
  137. }
  138. /**
  139. * Creates the <span> element containing the title and the link to the topic associated to a node in the menu or the TOC.
  140. *
  141. * @param topic The JSON object containing the info about the associated node.
  142. *
  143. * @returns {*|jQuery|HTMLElement} the topic title 'span' element.
  144. */
  145. function createTopicRefSpan(topic) {
  146. var isExternalReference = topic.scope == 'external';
  147. // .topicref span
  148. var topicRefSpan = $("<span>");
  149. topicRefSpan.addClass("topicref");
  150. if (topic.outputclass != null) {
  151. topicRefSpan.addClass(topic.outputclass);
  152. }
  153. // WH-1820 Copy the Ditaval "pass through" attributes.
  154. var dataAttributes = topic.attributes;
  155. if (typeof dataAttributes !== 'undefined') {
  156. var attrsNames = Object.keys(dataAttributes);
  157. attrsNames.forEach(function(attr) {
  158. topicRefSpan.attr(attr, dataAttributes[attr]);
  159. });
  160. }
  161. topicRefSpan.attr(navConfig.attrs.tocID, topic.tocID);
  162. topicRefSpan.attr("id", topic.tocID + "-mi");
  163. // Current node state
  164. var containsChildren = hasChildren(topic);
  165. if (containsChildren) {
  166. // This state means that the child topics should be retrieved later.
  167. topicRefSpan.attr(navConfig.attrs.state, navConfig.states.notReady);
  168. } else {
  169. topicRefSpan.attr(navConfig.attrs.state, navConfig.states.leaf);
  170. }
  171. // Topic ref link
  172. var linkHref = '';
  173. if (topic.href != null && topic.href != 'javascript:void(0)') {
  174. if (!isExternalReference) {
  175. linkHref += getPathToRoot();
  176. }
  177. linkHref += topic.href;
  178. }
  179. var link = $("<a>", {
  180. href: linkHref,
  181. html: topic.title
  182. });
  183. // WH-2368 Update the relative links
  184. var pathToRoot = getPathToRoot();
  185. var linksInLink = link.find("a[href]");
  186. linksInLink.each(function () {
  187. var href = $(this).attr("href");
  188. if (!(href.startsWith("http:") || href.startsWith("https:"))) {
  189. $(this).attr("href", pathToRoot + href);
  190. }
  191. });
  192. var imgsInLink = link.find("img[src]");
  193. imgsInLink.each(function () {
  194. var src = $(this).attr("src");
  195. if (!(src.startsWith("http:") || src.startsWith("https:"))) {
  196. $(this).attr("src", pathToRoot + src);
  197. }
  198. });
  199. if (isExternalReference) {
  200. link.attr("target", "_blank");
  201. }
  202. var titleSpan = $("<span>", {
  203. class: "title"
  204. });
  205. titleSpan.append(link);
  206. topicRefSpan.append(titleSpan);
  207. return topicRefSpan;
  208. }
  209. function hasChildren(topic) {
  210. // If the "topics" property is not specified then it means that children should be loaded from the
  211. // module referenced in the "next" property
  212. var children = topic.topics;
  213. var hasChildren;
  214. if (children != null && children.length == 0) {
  215. hasChildren = false;
  216. } else if (topic.menu != null) {
  217. hasChildren = topic.menu.hasChildren;
  218. } else {
  219. hasChildren = true;
  220. }
  221. return hasChildren;
  222. }
  223. function menuItemHovered() {
  224. var topicRefSpan = $(this).children('.topicref');
  225. var state = topicRefSpan.attr(navConfig.attrs.state);
  226. if (state === navConfig.states.pending) {
  227. // Do nothing
  228. } else if (state === navConfig.states.notReady) {
  229. topicRefSpan.attr(navConfig.attrs.state, navConfig.states.pending);
  230. var menuItemID = topicRefSpan.attr("id");
  231. var dot = $("<span>", {
  232. class: "dot"
  233. });
  234. var loadingMarker =
  235. $("<ul>", {
  236. class: "loading",
  237. "aria-labelledby" : menuItemID,
  238. role : "menu",
  239. html: $("<li>", {
  240. role : "menuitem",
  241. html: [dot, dot.clone(), dot.clone()]
  242. })
  243. });
  244. $(this).append(loadingMarker);
  245. handleMenuPosition($(this));
  246. retrieveChildNodes(topicRefSpan);
  247. } else if (state === navConfig.states.expanded) {
  248. handleMenuPosition($(this));
  249. } else if (state === navConfig.states.collapsed) {
  250. topicRefSpan.attr(navConfig.attrs.state, navConfig.states.expanded);
  251. }
  252. if ($(this).attr("aria-expanded") != null) {
  253. $(this).attr("aria-expanded", "true");
  254. }
  255. };
  256. var dirAttr = $('html').attr('dir');
  257. var rtlEnabled = false;
  258. if (dirAttr=='rtl') {
  259. rtlEnabled = true;
  260. }
  261. /**
  262. * Display top menu so that it will not exit the viewport.
  263. *
  264. * @param $menuItem The top menu menu 'li' element of the current node from TOC / Menu.
  265. */
  266. function handleMenuPosition($menuItem) {
  267. // Handle menu position
  268. var parentDir = rtlEnabled ? 'left' : 'right';
  269. var index = $('.wh_top_menu ul').index($menuItem.parent('ul'));
  270. var currentElementOffsetLeft = parseInt($menuItem.offset().left);
  271. var currentElementWidth = parseInt($menuItem.width());
  272. var currentElementOffsetRight = currentElementOffsetLeft + currentElementWidth;
  273. var nextElementWidth = parseInt($menuItem.children('ul').width());
  274. var offsetLeft,
  275. offsetRight = currentElementOffsetRight + nextElementWidth;
  276. if (index == 0) {
  277. if (parentDir=='left') {
  278. $menuItem.attr('data-menuDirection', 'left');
  279. offsetLeft = currentElementOffsetRight - nextElementWidth;
  280. if (offsetLeft <= 0) {
  281. $menuItem.css('position', 'relative');
  282. $menuItem.children('ul').css('position','absolute');
  283. $menuItem.children('ul').css('right', 'auto');
  284. $menuItem.children('ul').css('left', '0');
  285. }
  286. } else {
  287. $menuItem.attr('data-menuDirection', 'right');
  288. offsetRight = currentElementOffsetLeft + nextElementWidth;
  289. if (offsetRight >= $(window).width()) {
  290. $menuItem.css('position', 'relative');
  291. $menuItem.children('ul').css('position','absolute');
  292. $menuItem.children('ul').css('right', '0');
  293. $menuItem.children('ul').css('left', 'auto');
  294. }
  295. }
  296. } else {
  297. offsetLeft = currentElementOffsetLeft - nextElementWidth;
  298. if (parentDir == 'left') {
  299. if (offsetLeft >= 0) {
  300. $menuItem.attr('data-menuDirection', 'left');
  301. $menuItem.children('ul').css('right', '100%');
  302. $menuItem.children('ul').css('left', 'auto');
  303. } else {
  304. $menuItem.attr('data-menuDirection', 'right');
  305. $menuItem.children('ul').css('right', 'auto');
  306. $menuItem.children('ul').css('left', '100%');
  307. }
  308. } else {
  309. if (offsetRight <= $(window).width()) {
  310. $menuItem.attr('data-menuDirection', 'right');
  311. $menuItem.children('ul').css('right', 'auto');
  312. $menuItem.children('ul').css('left', '100%');
  313. } else {
  314. $menuItem.attr('data-menuDirection', 'left');
  315. $menuItem.children('ul').css('right', '100%');
  316. $menuItem.children('ul').css('left', 'auto');
  317. }
  318. }
  319. }
  320. }
  321. });