admin_menu.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. (function($) {
  2. Drupal.admin = Drupal.admin || {};
  3. Drupal.admin.behaviors = Drupal.admin.behaviors || {};
  4. Drupal.admin.hashes = Drupal.admin.hashes || {};
  5. /**
  6. * Core behavior for Administration menu.
  7. *
  8. * Test whether there is an administration menu is in the output and execute all
  9. * registered behaviors.
  10. */
  11. Drupal.behaviors.adminMenu = {
  12. attach: function (context, settings) {
  13. // Initialize settings.
  14. settings.admin_menu = $.extend({
  15. suppress: false,
  16. margin_top: false,
  17. position_fixed: false,
  18. tweak_modules: false,
  19. tweak_permissions: false,
  20. tweak_tabs: false,
  21. destination: '',
  22. basePath: settings.basePath,
  23. hash: 0,
  24. replacements: {}
  25. }, settings.admin_menu || {});
  26. // Check whether administration menu should be suppressed.
  27. if (settings.admin_menu.suppress) {
  28. return;
  29. }
  30. var $adminMenu = $('#admin-menu:not(.admin-menu-processed)', context);
  31. // Client-side caching; if administration menu is not in the output, it is
  32. // fetched from the server and cached in the browser.
  33. if (!$adminMenu.length && settings.admin_menu.hash) {
  34. Drupal.admin.getCache(settings.admin_menu.hash, function (response) {
  35. if (typeof response == 'string' && response.length > 0) {
  36. $('body', context).append(response);
  37. }
  38. var $adminMenu = $('#admin-menu:not(.admin-menu-processed)', context);
  39. // Apply our behaviors.
  40. Drupal.admin.attachBehaviors(context, settings, $adminMenu);
  41. // Allow resize event handlers to recalculate sizes/positions.
  42. $(window).triggerHandler('resize');
  43. });
  44. }
  45. // If the menu is in the output already, this means there is a new version.
  46. else {
  47. // Apply our behaviors.
  48. Drupal.admin.attachBehaviors(context, settings, $adminMenu);
  49. }
  50. }
  51. };
  52. /**
  53. * Collapse fieldsets on Modules page.
  54. */
  55. Drupal.behaviors.adminMenuCollapseModules = {
  56. attach: function (context, settings) {
  57. if (settings.admin_menu.tweak_modules) {
  58. $('#system-modules fieldset:not(.collapsed)', context).addClass('collapsed');
  59. }
  60. }
  61. };
  62. /**
  63. * Collapse modules on Permissions page.
  64. */
  65. Drupal.behaviors.adminMenuCollapsePermissions = {
  66. attach: function (context, settings) {
  67. if (settings.admin_menu.tweak_permissions) {
  68. // Freeze width of first column to prevent jumping.
  69. $('#permissions th:first', context).css({ width: $('#permissions th:first', context).width() });
  70. // Attach click handler.
  71. $modules = $('#permissions tr:has(td.module)', context).once('admin-menu-tweak-permissions', function () {
  72. var $module = $(this);
  73. $module.bind('click.admin-menu', function () {
  74. // @todo Replace with .nextUntil() in jQuery 1.4.
  75. $module.nextAll().each(function () {
  76. var $row = $(this);
  77. if ($row.is(':has(td.module)')) {
  78. return false;
  79. }
  80. $row.toggleClass('element-hidden');
  81. });
  82. });
  83. });
  84. // Collapse all but the targeted permission rows set.
  85. if (window.location.hash.length) {
  86. $modules = $modules.not(':has(' + window.location.hash + ')');
  87. }
  88. $modules.trigger('click.admin-menu');
  89. }
  90. }
  91. };
  92. /**
  93. * Apply margin to page.
  94. *
  95. * Note that directly applying marginTop does not work in IE. To prevent
  96. * flickering/jumping page content with client-side caching, this is a regular
  97. * Drupal behavior.
  98. */
  99. Drupal.behaviors.adminMenuMarginTop = {
  100. attach: function (context, settings) {
  101. if (!settings.admin_menu.suppress && settings.admin_menu.margin_top) {
  102. $('body:not(.admin-menu)', context).addClass('admin-menu');
  103. }
  104. }
  105. };
  106. /**
  107. * Retrieve content from client-side cache.
  108. *
  109. * @param hash
  110. * The md5 hash of the content to retrieve.
  111. * @param onSuccess
  112. * A callback function invoked when the cache request was successful.
  113. */
  114. Drupal.admin.getCache = function (hash, onSuccess) {
  115. if (Drupal.admin.hashes.hash !== undefined) {
  116. return Drupal.admin.hashes.hash;
  117. }
  118. $.ajax({
  119. cache: true,
  120. type: 'GET',
  121. dataType: 'text', // Prevent auto-evaluation of response.
  122. global: false, // Do not trigger global AJAX events.
  123. url: Drupal.settings.admin_menu.basePath.replace(/admin_menu/, 'js/admin_menu/cache/' + hash),
  124. success: onSuccess,
  125. complete: function (XMLHttpRequest, status) {
  126. Drupal.admin.hashes.hash = status;
  127. }
  128. });
  129. };
  130. /**
  131. * TableHeader callback to determine top viewport offset.
  132. *
  133. * @see toolbar.js
  134. */
  135. Drupal.admin.height = function() {
  136. var $adminMenu = $('#admin-menu');
  137. var height = $adminMenu.outerHeight();
  138. // In IE, Shadow filter adds some extra height, so we need to remove it from
  139. // the returned height.
  140. if ($adminMenu.css('filter') && $adminMenu.css('filter').match(/DXImageTransform\.Microsoft\.Shadow/)) {
  141. height -= $adminMenu.get(0).filters.item("DXImageTransform.Microsoft.Shadow").strength;
  142. }
  143. return height;
  144. };
  145. /**
  146. * @defgroup admin_behaviors Administration behaviors.
  147. * @{
  148. */
  149. /**
  150. * Attach administrative behaviors.
  151. */
  152. Drupal.admin.attachBehaviors = function (context, settings, $adminMenu) {
  153. if ($adminMenu.length) {
  154. $adminMenu.addClass('admin-menu-processed');
  155. $.each(Drupal.admin.behaviors, function() {
  156. this(context, settings, $adminMenu);
  157. });
  158. }
  159. };
  160. /**
  161. * Apply 'position: fixed'.
  162. */
  163. Drupal.admin.behaviors.positionFixed = function (context, settings, $adminMenu) {
  164. if (settings.admin_menu.position_fixed) {
  165. $adminMenu.addClass('admin-menu-position-fixed');
  166. $adminMenu.css('position', 'fixed');
  167. }
  168. };
  169. /**
  170. * Move page tabs into administration menu.
  171. */
  172. Drupal.admin.behaviors.pageTabs = function (context, settings, $adminMenu) {
  173. if (settings.admin_menu.tweak_tabs) {
  174. var $tabs = $(context).find('ul.tabs.primary');
  175. $adminMenu.find('#admin-menu-wrapper > ul').eq(1)
  176. .append($tabs.find('li').addClass('admin-menu-tab'));
  177. $(context).find('ul.tabs.secondary')
  178. .appendTo('#admin-menu-wrapper > ul > li.admin-menu-tab.active')
  179. .removeClass('secondary');
  180. $tabs.remove();
  181. }
  182. };
  183. /**
  184. * Perform dynamic replacements in cached menu.
  185. */
  186. Drupal.admin.behaviors.replacements = function (context, settings, $adminMenu) {
  187. for (var item in settings.admin_menu.replacements) {
  188. $(item, $adminMenu).html(settings.admin_menu.replacements[item]);
  189. }
  190. };
  191. /**
  192. * Inject destination query strings for current page.
  193. */
  194. Drupal.admin.behaviors.destination = function (context, settings, $adminMenu) {
  195. if (settings.admin_menu.destination) {
  196. $('a.admin-menu-destination', $adminMenu).each(function() {
  197. this.search += (!this.search.length ? '?' : '&') + Drupal.settings.admin_menu.destination;
  198. });
  199. }
  200. };
  201. /**
  202. * Apply JavaScript-based hovering behaviors.
  203. *
  204. * @todo This has to run last. If another script registers additional behaviors
  205. * it will not run last.
  206. */
  207. Drupal.admin.behaviors.hover = function (context, settings, $adminMenu) {
  208. // Hover emulation for IE 6.
  209. if ($.browser.msie && parseInt(jQuery.browser.version) == 6) {
  210. $('li', $adminMenu).hover(
  211. function () {
  212. $(this).addClass('iehover');
  213. },
  214. function () {
  215. $(this).removeClass('iehover');
  216. }
  217. );
  218. }
  219. // Delayed mouseout.
  220. $('li.expandable', $adminMenu).hover(
  221. function () {
  222. // Stop the timer.
  223. clearTimeout(this.sfTimer);
  224. // Display child lists.
  225. $('> ul', this)
  226. .css({left: 'auto', display: 'block'})
  227. // Immediately hide nephew lists.
  228. .parent().siblings('li').children('ul').css({left: '-999em', display: 'none'});
  229. },
  230. function () {
  231. // Start the timer.
  232. var uls = $('> ul', this);
  233. this.sfTimer = setTimeout(function () {
  234. uls.css({left: '-999em', display: 'none'});
  235. }, 400);
  236. }
  237. );
  238. };
  239. /**
  240. * Apply the search bar functionality.
  241. */
  242. Drupal.admin.behaviors.search = function (context, settings, $adminMenu) {
  243. // @todo Add a HTML ID.
  244. var $input = $('input.admin-menu-search', $adminMenu);
  245. // Initialize the current search needle.
  246. var needle = $input.val();
  247. // Cache of all links that can be matched in the menu.
  248. var links;
  249. // Minimum search needle length.
  250. var needleMinLength = 2;
  251. // Append the results container.
  252. var $results = $('<div />').insertAfter($input);
  253. /**
  254. * Executes the search upon user input.
  255. */
  256. function keyupHandler() {
  257. var matches, $html, value = $(this).val();
  258. // Only proceed if the search needle has changed.
  259. if (value !== needle) {
  260. needle = value;
  261. // Initialize the cache of menu links upon first search.
  262. if (!links && needle.length >= needleMinLength) {
  263. // @todo Limit to links in dropdown menus; i.e., skip menu additions.
  264. links = buildSearchIndex($adminMenu.find('li:not(.admin-menu-action, .admin-menu-action li) > a'));
  265. }
  266. // Empty results container when deleting search text.
  267. if (needle.length < needleMinLength) {
  268. $results.empty();
  269. }
  270. // Only search if the needle is long enough.
  271. if (needle.length >= needleMinLength && links) {
  272. matches = findMatches(needle, links);
  273. // Build the list in a detached DOM node.
  274. $html = buildResultsList(matches);
  275. // Display results.
  276. $results.empty().append($html);
  277. }
  278. }
  279. }
  280. /**
  281. * Builds the search index.
  282. */
  283. function buildSearchIndex($links) {
  284. return $links
  285. .map(function () {
  286. var text = (this.textContent || this.innerText);
  287. // Skip menu entries that do not contain any text (e.g., the icon).
  288. if (typeof text === 'undefined') {
  289. return;
  290. }
  291. return {
  292. text: text,
  293. textMatch: text.toLowerCase(),
  294. element: this
  295. };
  296. });
  297. }
  298. /**
  299. * Searches the index for a given needle and returns matching entries.
  300. */
  301. function findMatches(needle, links) {
  302. var needleMatch = needle.toLowerCase();
  303. // Select matching links from the cache.
  304. return $.grep(links, function (link) {
  305. return link.textMatch.indexOf(needleMatch) !== -1;
  306. });
  307. }
  308. /**
  309. * Builds the search result list in a detached DOM node.
  310. */
  311. function buildResultsList(matches) {
  312. var $html = $('<ul class="dropdown admin-menu-search-results" />');
  313. $.each(matches, function () {
  314. var result = this.text;
  315. var $element = $(this.element);
  316. // Check whether there is a top-level category that can be prepended.
  317. var $category = $element.closest('#admin-menu-wrapper > ul > li');
  318. var categoryText = $category.find('> a').text()
  319. if ($category.length && categoryText) {
  320. result = categoryText + ': ' + result;
  321. }
  322. var $result = $('<li><a href="' + $element.attr('href') + '">' + result + '</a></li>');
  323. $result.data('original-link', $(this.element).parent());
  324. $html.append($result);
  325. });
  326. return $html;
  327. }
  328. /**
  329. * Highlights selected result.
  330. */
  331. function resultsHandler(e) {
  332. var $this = $(this);
  333. var show = e.type === 'mouseenter' || e.type === 'focusin';
  334. $this.trigger(show ? 'showPath' : 'hidePath', [this]);
  335. }
  336. /**
  337. * Closes the search results and clears the search input.
  338. */
  339. function resultsClickHandler(e, link) {
  340. var $original = $(this).data('original-link');
  341. $original.trigger('mouseleave');
  342. $input.val('').trigger('keyup');
  343. }
  344. /**
  345. * Shows the link in the menu that corresponds to a search result.
  346. */
  347. function highlightPathHandler(e, link) {
  348. if (link) {
  349. var $original = $(link).data('original-link');
  350. var show = e.type === 'showPath';
  351. // Toggle an additional CSS class to visually highlight the matching link.
  352. // @todo Consider using same visual appearance as regular hover.
  353. $original.toggleClass('highlight', show);
  354. $original.trigger(show ? 'mouseenter' : 'mouseleave');
  355. }
  356. }
  357. // Attach showPath/hidePath handler to search result entries.
  358. $results.delegate('li', 'mouseenter mouseleave focus blur', resultsHandler);
  359. // Hide the result list after a link has been clicked, useful for overlay.
  360. $results.delegate('li', 'click', resultsClickHandler);
  361. // Attach hover/active highlight behavior to search result entries.
  362. $adminMenu.delegate('.admin-menu-search-results li', 'showPath hidePath', highlightPathHandler);
  363. // Attach the search input event handler.
  364. $input.bind('keyup search', keyupHandler);
  365. };
  366. /**
  367. * @} End of "defgroup admin_behaviors".
  368. */
  369. })(jQuery);