overlay-parent.js 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026
  1. /**
  2. * Modified by ericduran for jQuery Update compatibility.
  3. *
  4. * Patches overlay-parent.js for jQuery 1.9.1 compatibility.
  5. */
  6. /**
  7. * @file
  8. * Attaches the behaviors for the Overlay parent pages.
  9. */
  10. (function ($) {
  11. /**
  12. * Open the overlay, or load content into it, when an admin link is clicked.
  13. */
  14. Drupal.behaviors.overlayParent = {
  15. attach: function (context, settings) {
  16. if (Drupal.overlay.isOpen) {
  17. Drupal.overlay.makeDocumentUntabbable(context);
  18. }
  19. if (this.processed) {
  20. return;
  21. }
  22. this.processed = true;
  23. $(window)
  24. // When the hash (URL fragment) changes, open the overlay if needed.
  25. .bind('hashchange.drupal-overlay', $.proxy(Drupal.overlay, 'eventhandlerOperateByURLFragment'))
  26. // Trigger the hashchange handler once, after the page is loaded, so that
  27. // permalinks open the overlay.
  28. .triggerHandler('hashchange.drupal-overlay');
  29. $(document)
  30. // Instead of binding a click event handler to every link we bind one to
  31. // the document and only handle events that bubble up. This allows other
  32. // scripts to bind their own handlers to links and also to prevent
  33. // overlay's handling.
  34. .bind('click.drupal-overlay mouseup.drupal-overlay', $.proxy(Drupal.overlay, 'eventhandlerOverrideLink'));
  35. }
  36. };
  37. /**
  38. * Overlay object for parent windows.
  39. *
  40. * Events
  41. * Overlay triggers a number of events that can be used by other scripts.
  42. * - drupalOverlayOpen: This event is triggered when the overlay is opened.
  43. * - drupalOverlayBeforeClose: This event is triggered when the overlay attempts
  44. * to close. If an event handler returns false, the close will be prevented.
  45. * - drupalOverlayClose: This event is triggered when the overlay is closed.
  46. * - drupalOverlayBeforeLoad: This event is triggered right before a new URL
  47. * is loaded into the overlay.
  48. * - drupalOverlayReady: This event is triggered when the DOM of the overlay
  49. * child document is fully loaded.
  50. * - drupalOverlayLoad: This event is triggered when the overlay is finished
  51. * loading.
  52. * - drupalOverlayResize: This event is triggered when the overlay is being
  53. * resized to match the parent window.
  54. */
  55. Drupal.overlay = Drupal.overlay || {
  56. isOpen: false,
  57. isOpening: false,
  58. isClosing: false,
  59. isLoading: false
  60. };
  61. Drupal.overlay.prototype = {};
  62. /**
  63. * Open the overlay.
  64. *
  65. * @param url
  66. * The URL of the page to open in the overlay.
  67. *
  68. * @return
  69. * TRUE if the overlay was opened, FALSE otherwise.
  70. */
  71. Drupal.overlay.open = function (url) {
  72. // Just one overlay is allowed.
  73. if (this.isOpen || this.isOpening) {
  74. return this.load(url);
  75. }
  76. this.isOpening = true;
  77. // Store the original document title.
  78. this.originalTitle = document.title;
  79. // Create the dialog and related DOM elements.
  80. this.create();
  81. this.isOpening = false;
  82. this.isOpen = true;
  83. $(document.documentElement).addClass('overlay-open');
  84. this.makeDocumentUntabbable();
  85. // Allow other scripts to respond to this event.
  86. $(document).trigger('drupalOverlayOpen');
  87. return this.load(url);
  88. };
  89. /**
  90. * Create the underlying markup and behaviors for the overlay.
  91. */
  92. Drupal.overlay.create = function () {
  93. this.$container = $(Drupal.theme('overlayContainer'))
  94. .appendTo(document.body);
  95. // Overlay uses transparent iframes that cover the full parent window.
  96. // When the overlay is open the scrollbar of the parent window is hidden.
  97. // Because some browsers show a white iframe background for a short moment
  98. // while loading a page into an iframe, overlay uses two iframes. By loading
  99. // the page in a hidden (inactive) iframe the user doesn't see the white
  100. // background. When the page is loaded the active and inactive iframes
  101. // are switched.
  102. this.activeFrame = this.$iframeA = $(Drupal.theme('overlayElement'))
  103. .appendTo(this.$container);
  104. this.inactiveFrame = this.$iframeB = $(Drupal.theme('overlayElement'))
  105. .appendTo(this.$container);
  106. this.$iframeA.bind('load.drupal-overlay', { self: this.$iframeA[0], sibling: this.$iframeB }, $.proxy(this, 'loadChild'));
  107. this.$iframeB.bind('load.drupal-overlay', { self: this.$iframeB[0], sibling: this.$iframeA }, $.proxy(this, 'loadChild'));
  108. // Add a second class "drupal-overlay-open" to indicate these event handlers
  109. // should only be bound when the overlay is open.
  110. var eventClass = '.drupal-overlay.drupal-overlay-open';
  111. $(window)
  112. .bind('resize' + eventClass, $.proxy(this, 'eventhandlerOuterResize'));
  113. $(document)
  114. .bind('drupalOverlayLoad' + eventClass, $.proxy(this, 'eventhandlerOuterResize'))
  115. .bind('drupalOverlayReady' + eventClass +
  116. ' drupalOverlayClose' + eventClass, $.proxy(this, 'eventhandlerSyncURLFragment'))
  117. .bind('drupalOverlayClose' + eventClass, $.proxy(this, 'eventhandlerRefreshPage'))
  118. .bind('drupalOverlayBeforeClose' + eventClass +
  119. ' drupalOverlayBeforeLoad' + eventClass +
  120. ' drupalOverlayResize' + eventClass, $.proxy(this, 'eventhandlerDispatchEvent'));
  121. if ($('.overlay-displace-top, .overlay-displace-bottom').length) {
  122. $(document)
  123. .bind('drupalOverlayResize' + eventClass, $.proxy(this, 'eventhandlerAlterDisplacedElements'))
  124. .bind('drupalOverlayClose' + eventClass, $.proxy(this, 'eventhandlerRestoreDisplacedElements'));
  125. }
  126. };
  127. /**
  128. * Load the given URL into the overlay iframe.
  129. *
  130. * Use this method to change the URL being loaded in the overlay if it is
  131. * already open.
  132. *
  133. * @return
  134. * TRUE if URL is loaded into the overlay, FALSE otherwise.
  135. */
  136. Drupal.overlay.load = function (url) {
  137. if (!this.isOpen) {
  138. return false;
  139. }
  140. // Allow other scripts to respond to this event.
  141. $(document).trigger('drupalOverlayBeforeLoad');
  142. $(document.documentElement).addClass('overlay-loading');
  143. // The contentDocument property is not supported in IE until IE8.
  144. var iframeDocument = this.inactiveFrame[0].contentDocument || this.inactiveFrame[0].contentWindow.document;
  145. // location.replace doesn't create a history entry. location.href does.
  146. // In this case, we want location.replace, as we're creating the history
  147. // entry using URL fragments.
  148. iframeDocument.location.replace(url);
  149. return true;
  150. };
  151. /**
  152. * Close the overlay and remove markup related to it from the document.
  153. *
  154. * @return
  155. * TRUE if the overlay was closed, FALSE otherwise.
  156. */
  157. Drupal.overlay.close = function () {
  158. // Prevent double execution when close is requested more than once.
  159. if (!this.isOpen || this.isClosing) {
  160. return false;
  161. }
  162. // Allow other scripts to respond to this event.
  163. var event = $.Event('drupalOverlayBeforeClose');
  164. $(document).trigger(event);
  165. // If a handler returned false, the close will be prevented.
  166. if (event.isDefaultPrevented()) {
  167. return false;
  168. }
  169. this.isClosing = true;
  170. this.isOpen = false;
  171. $(document.documentElement).removeClass('overlay-open');
  172. // Restore the original document title.
  173. document.title = this.originalTitle;
  174. this.makeDocumentTabbable();
  175. // Allow other scripts to respond to this event.
  176. $(document).trigger('drupalOverlayClose');
  177. // When the iframe is still loading don't destroy it immediately but after
  178. // the content is loaded (see Drupal.overlay.loadChild).
  179. if (!this.isLoading) {
  180. this.destroy();
  181. this.isClosing = false;
  182. }
  183. return true;
  184. };
  185. /**
  186. * Destroy the overlay.
  187. */
  188. Drupal.overlay.destroy = function () {
  189. $([document, window]).unbind('.drupal-overlay-open');
  190. this.$container.remove();
  191. this.$container = null;
  192. this.$iframeA = null;
  193. this.$iframeB = null;
  194. this.iframeWindow = null;
  195. };
  196. /**
  197. * Redirect the overlay parent window to the given URL.
  198. *
  199. * @param url
  200. * Can be an absolute URL or a relative link to the domain root.
  201. */
  202. Drupal.overlay.redirect = function (url) {
  203. // Create a native Link object, so we can use its object methods.
  204. var link = $(url.link(url)).get(0);
  205. // If the link is already open, force the hashchange event to simulate reload.
  206. if (window.location.href == link.href) {
  207. $(window).triggerHandler('hashchange.drupal-overlay');
  208. }
  209. window.location.href = link.href;
  210. return true;
  211. };
  212. /**
  213. * Bind the child window.
  214. *
  215. * Note that this function is fired earlier than Drupal.overlay.loadChild.
  216. */
  217. Drupal.overlay.bindChild = function (iframeWindow, isClosing) {
  218. this.iframeWindow = iframeWindow;
  219. // We are done if the child window is closing.
  220. if (isClosing || this.isClosing || !this.isOpen) {
  221. return;
  222. }
  223. // Allow other scripts to respond to this event.
  224. $(document).trigger('drupalOverlayReady');
  225. };
  226. /**
  227. * Event handler: load event handler for the overlay iframe.
  228. *
  229. * @param event
  230. * Event being triggered, with the following restrictions:
  231. * - event.type: load
  232. * - event.currentTarget: iframe
  233. */
  234. Drupal.overlay.loadChild = function (event) {
  235. var iframe = event.data.self;
  236. var iframeDocument = iframe.contentDocument || iframe.contentWindow.document;
  237. var iframeWindow = iframeDocument.defaultView || iframeDocument.parentWindow;
  238. if (iframeWindow.location == 'about:blank') {
  239. return;
  240. }
  241. this.isLoading = false;
  242. $(document.documentElement).removeClass('overlay-loading');
  243. event.data.sibling.removeClass('overlay-active').attr({ 'tabindex': -1 });
  244. // Only continue when overlay is still open and not closing.
  245. if (this.isOpen && !this.isClosing) {
  246. // And child document is an actual overlayChild.
  247. if (iframeWindow.Drupal && iframeWindow.Drupal.overlayChild) {
  248. // Replace the document title with title of iframe.
  249. document.title = iframeWindow.document.title;
  250. this.activeFrame = $(iframe)
  251. .addClass('overlay-active')
  252. // Add a title attribute to the iframe for accessibility.
  253. .attr('title', Drupal.t('@title dialog', { '@title': iframeWindow.jQuery('#overlay-title').text() })).removeAttr('tabindex');
  254. this.inactiveFrame = event.data.sibling;
  255. // Load an empty document into the inactive iframe.
  256. (this.inactiveFrame[0].contentDocument || this.inactiveFrame[0].contentWindow.document).location.replace('about:blank');
  257. // Move the focus to just before the "skip to main content" link inside
  258. // the overlay.
  259. this.activeFrame.focus();
  260. var skipLink = iframeWindow.jQuery('a:first');
  261. Drupal.overlay.setFocusBefore(skipLink, iframeWindow.document);
  262. // Allow other scripts to respond to this event.
  263. $(document).trigger('drupalOverlayLoad');
  264. }
  265. else {
  266. window.location = iframeWindow.location.href.replace(/([?&]?)render=overlay&?/g, '$1').replace(/\?$/, '');
  267. }
  268. }
  269. else {
  270. this.destroy();
  271. }
  272. };
  273. /**
  274. * Creates a placeholder element to receive document focus.
  275. *
  276. * Setting the document focus to a link will make it visible, even if it's a
  277. * "skip to main content" link that should normally be visible only when the
  278. * user tabs to it. This function can be used to set the document focus to
  279. * just before such an invisible link.
  280. *
  281. * @param $element
  282. * The jQuery element that should receive focus on the next tab press.
  283. * @param document
  284. * The iframe window element to which the placeholder should be added. The
  285. * placeholder element has to be created inside the same iframe as the element
  286. * it precedes, to keep IE happy. (http://bugs.jquery.com/ticket/4059)
  287. */
  288. Drupal.overlay.setFocusBefore = function ($element, document) {
  289. // Create an anchor inside the placeholder document.
  290. var placeholder = document.createElement('a');
  291. var $placeholder = $(placeholder).addClass('element-invisible').attr('href', '#');
  292. // Put the placeholder where it belongs, and set the document focus to it.
  293. $placeholder.insertBefore($element);
  294. $placeholder.focus();
  295. // Make the placeholder disappear as soon as it loses focus, so that it
  296. // doesn't appear in the tab order again.
  297. $placeholder.one('blur', function () {
  298. $(this).remove();
  299. });
  300. };
  301. /**
  302. * Check if the given link is in the administrative section of the site.
  303. *
  304. * @param url
  305. * The URL to be tested.
  306. *
  307. * @return boolean
  308. * TRUE if the URL represents an administrative link, FALSE otherwise.
  309. */
  310. Drupal.overlay.isAdminLink = function (url) {
  311. if (Drupal.overlay.isExternalLink(url)) {
  312. return false;
  313. }
  314. var path = this.getPath(url);
  315. // Turn the list of administrative paths into a regular expression.
  316. if (!this.adminPathRegExp) {
  317. var prefix = '';
  318. if (Drupal.settings.overlay.pathPrefixes.length) {
  319. // Allow path prefixes used for language negatiation followed by slash,
  320. // and the empty string.
  321. prefix = '(' + Drupal.settings.overlay.pathPrefixes.join('/|') + '/|)';
  322. }
  323. var adminPaths = '^' + prefix + '(' + Drupal.settings.overlay.paths.admin.replace(/\s+/g, '|') + ')$';
  324. var nonAdminPaths = '^' + prefix + '(' + Drupal.settings.overlay.paths.non_admin.replace(/\s+/g, '|') + ')$';
  325. adminPaths = adminPaths.replace(/\*/g, '.*');
  326. nonAdminPaths = nonAdminPaths.replace(/\*/g, '.*');
  327. this.adminPathRegExp = new RegExp(adminPaths);
  328. this.nonAdminPathRegExp = new RegExp(nonAdminPaths);
  329. }
  330. return this.adminPathRegExp.exec(path) && !this.nonAdminPathRegExp.exec(path);
  331. };
  332. /**
  333. * Determine whether a link is external to the site.
  334. *
  335. * Deprecated. Use Drupal.urlIsLocal() instead.
  336. *
  337. * @param url
  338. * The URL to be tested.
  339. *
  340. * @return boolean
  341. * TRUE if the URL is external to the site, FALSE otherwise.
  342. */
  343. Drupal.overlay.isExternalLink = function (url) {
  344. // Drupal.urlIsLocal() was added in Drupal 7.39.
  345. if (typeof Drupal.urlIsLocal === 'function') {
  346. return !Drupal.urlIsLocal(url);
  347. }
  348. var re = RegExp('^((f|ht)tps?:)?//(?!' + window.location.host + ')');
  349. return re.test(url);
  350. };
  351. /**
  352. * Constructs an internal URL (relative to this site) from the provided path.
  353. *
  354. * For example, if the provided path is 'admin' and the site is installed at
  355. * http://example.com/drupal, this function will return '/drupal/admin'.
  356. *
  357. * @param path
  358. * The internal path, without any leading slash.
  359. *
  360. * @return
  361. * The internal URL derived from the provided path, or null if a valid
  362. * internal path cannot be constructed (for example, if an attempt to create
  363. * an external link is detected).
  364. */
  365. Drupal.overlay.getInternalUrl = function (path) {
  366. var url = Drupal.settings.basePath + path;
  367. if (!this.isExternalLink(url)) {
  368. return url;
  369. }
  370. };
  371. /**
  372. * Event handler: resizes overlay according to the size of the parent window.
  373. *
  374. * @param event
  375. * Event being triggered, with the following restrictions:
  376. * - event.type: any
  377. * - event.currentTarget: any
  378. */
  379. Drupal.overlay.eventhandlerOuterResize = function (event) {
  380. // Proceed only if the overlay still exists.
  381. if (!(this.isOpen || this.isOpening) || this.isClosing || !this.iframeWindow) {
  382. return;
  383. }
  384. // IE6 uses position:absolute instead of position:fixed.
  385. if (typeof document.body.style.maxHeight != 'string') {
  386. this.activeFrame.height($(window).height());
  387. }
  388. // Allow other scripts to respond to this event.
  389. $(document).trigger('drupalOverlayResize');
  390. };
  391. /**
  392. * Event handler: resizes displaced elements so they won't overlap the scrollbar
  393. * of overlay's iframe.
  394. *
  395. * @param event
  396. * Event being triggered, with the following restrictions:
  397. * - event.type: any
  398. * - event.currentTarget: any
  399. */
  400. Drupal.overlay.eventhandlerAlterDisplacedElements = function (event) {
  401. // Proceed only if the overlay still exists.
  402. if (!(this.isOpen || this.isOpening) || this.isClosing || !this.iframeWindow) {
  403. return;
  404. }
  405. $(this.iframeWindow.document.body).css({
  406. marginTop: Drupal.overlay.getDisplacement('top'),
  407. marginBottom: Drupal.overlay.getDisplacement('bottom')
  408. })
  409. // IE7 isn't reflowing the document immediately.
  410. // @todo This might be fixed in a cleaner way.
  411. .addClass('overlay-trigger-reflow').removeClass('overlay-trigger-reflow');
  412. var documentHeight = this.iframeWindow.document.body.clientHeight;
  413. var documentWidth = this.iframeWindow.document.body.clientWidth;
  414. // IE6 doesn't support maxWidth, use width instead.
  415. var maxWidthName = (typeof document.body.style.maxWidth == 'string') ? 'maxWidth' : 'width';
  416. if (Drupal.overlay.leftSidedScrollbarOffset === undefined && $(document.documentElement).attr('dir') === 'rtl') {
  417. // We can't use element.clientLeft to detect whether scrollbars are placed
  418. // on the left side of the element when direction is set to "rtl" as most
  419. // browsers dont't support it correctly.
  420. // http://www.gtalbot.org/BugzillaSection/DocumentAllDHTMLproperties.html
  421. // There seems to be absolutely no way to detect whether the scrollbar
  422. // is on the left side in Opera; always expect scrollbar to be on the left.
  423. if ($.browser.opera) {
  424. Drupal.overlay.leftSidedScrollbarOffset = document.documentElement.clientWidth - this.iframeWindow.document.documentElement.clientWidth + this.iframeWindow.document.documentElement.clientLeft;
  425. }
  426. else if (this.iframeWindow.document.documentElement.clientLeft) {
  427. Drupal.overlay.leftSidedScrollbarOffset = this.iframeWindow.document.documentElement.clientLeft;
  428. }
  429. else {
  430. var el1 = $('<div style="direction: rtl; overflow: scroll;"></div>').appendTo(document.body);
  431. var el2 = $('<div></div>').appendTo(el1);
  432. Drupal.overlay.leftSidedScrollbarOffset = parseInt(el2[0].offsetLeft - el1[0].offsetLeft);
  433. el1.remove();
  434. }
  435. }
  436. // Consider any element that should be visible above the overlay (such as
  437. // a toolbar).
  438. $('.overlay-displace-top, .overlay-displace-bottom').each(function () {
  439. var data = $(this).data();
  440. var maxWidth = documentWidth;
  441. // In IE, Shadow filter makes element to overlap the scrollbar with 1px.
  442. if (this.filters && this.filters.length && this.filters.item('DXImageTransform.Microsoft.Shadow')) {
  443. maxWidth -= 1;
  444. }
  445. if (Drupal.overlay.leftSidedScrollbarOffset) {
  446. $(this).css('left', Drupal.overlay.leftSidedScrollbarOffset);
  447. }
  448. // Prevent displaced elements overlapping window's scrollbar.
  449. var currentMaxWidth = parseInt($(this).css(maxWidthName));
  450. if ((data.drupalOverlay && data.drupalOverlay.maxWidth) || isNaN(currentMaxWidth) || currentMaxWidth > maxWidth || currentMaxWidth <= 0) {
  451. $(this).css(maxWidthName, maxWidth);
  452. (data.drupalOverlay = data.drupalOverlay || {}).maxWidth = true;
  453. }
  454. // Use a more rigorous approach if the displaced element still overlaps
  455. // window's scrollbar; clip the element on the right.
  456. var offset = $(this).offset();
  457. var offsetRight = offset.left + $(this).outerWidth();
  458. if ((data.drupalOverlay && data.drupalOverlay.clip) || offsetRight > maxWidth) {
  459. if (Drupal.overlay.leftSidedScrollbarOffset) {
  460. $(this).css('clip', 'rect(auto, auto, ' + (documentHeight - offset.top) + 'px, ' + (Drupal.overlay.leftSidedScrollbarOffset + 2) + 'px)');
  461. }
  462. else {
  463. $(this).css('clip', 'rect(auto, ' + (maxWidth - offset.left) + 'px, ' + (documentHeight - offset.top) + 'px, auto)');
  464. }
  465. (data.drupalOverlay = data.drupalOverlay || {}).clip = true;
  466. }
  467. });
  468. };
  469. /**
  470. * Event handler: restores size of displaced elements as they were before
  471. * overlay was opened.
  472. *
  473. * @param event
  474. * Event being triggered, with the following restrictions:
  475. * - event.type: any
  476. * - event.currentTarget: any
  477. */
  478. Drupal.overlay.eventhandlerRestoreDisplacedElements = function (event) {
  479. var $displacedElements = $('.overlay-displace-top, .overlay-displace-bottom');
  480. try {
  481. $displacedElements.css({ maxWidth: '', clip: '' });
  482. }
  483. // IE bug that doesn't allow unsetting style.clip (http://dev.jquery.com/ticket/6512).
  484. catch (err) {
  485. $displacedElements.attr('style', function (index, attr) {
  486. return attr.replace(/clip\s*:\s*rect\([^)]+\);?/i, '');
  487. });
  488. }
  489. };
  490. /**
  491. * Event handler: overrides href of administrative links to be opened in
  492. * the overlay.
  493. *
  494. * This click event handler should be bound to any document (for example the
  495. * overlay iframe) of which you want links to open in the overlay.
  496. *
  497. * @param event
  498. * Event being triggered, with the following restrictions:
  499. * - event.type: click, mouseup
  500. * - event.currentTarget: document
  501. *
  502. * @see Drupal.overlayChild.behaviors.addClickHandler
  503. */
  504. Drupal.overlay.eventhandlerOverrideLink = function (event) {
  505. // In some browsers the click event isn't fired for right-clicks. Use the
  506. // mouseup event for right-clicks and the click event for everything else.
  507. if ((event.type == 'click' && event.button == 2) || (event.type == 'mouseup' && event.button != 2)) {
  508. return;
  509. }
  510. var $target = $(event.target);
  511. // Only continue if clicked target (or one of its parents) is a link.
  512. if (!$target.is('a')) {
  513. $target = $target.closest('a');
  514. if (!$target.length) {
  515. return;
  516. }
  517. }
  518. // Never open links in the overlay that contain the overlay-exclude class.
  519. if ($target.hasClass('overlay-exclude')) {
  520. return;
  521. }
  522. // Close the overlay when the link contains the overlay-close class.
  523. if ($target.hasClass('overlay-close')) {
  524. // Clearing the overlay URL fragment will close the overlay.
  525. $.bbq.removeState('overlay');
  526. return;
  527. }
  528. var target = $target[0];
  529. var href = target.href;
  530. // Only handle links that have an href attribute and use the HTTP(S) protocol.
  531. if (href != undefined && href != '' && target.protocol.match(/^https?\:/)) {
  532. var anchor = href.replace(target.ownerDocument.location.href, '');
  533. // Skip anchor links.
  534. if (anchor.length == 0 || anchor.charAt(0) == '#') {
  535. return;
  536. }
  537. // Open admin links in the overlay.
  538. else if (this.isAdminLink(href)) {
  539. // If the link contains the overlay-restore class and the overlay-context
  540. // state is set, also update the parent window's location.
  541. var parentLocation = ($target.hasClass('overlay-restore') && typeof $.bbq.getState('overlay-context') == 'string')
  542. ? this.getInternalUrl($.bbq.getState('overlay-context'))
  543. : null;
  544. href = this.fragmentizeLink($target.get(0), parentLocation);
  545. // Only override default behavior when left-clicking and user is not
  546. // pressing the ALT, CTRL, META (Command key on the Macintosh keyboard)
  547. // or SHIFT key.
  548. if (event.button == 0 && !event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey) {
  549. // Redirect to a fragmentized href. This will trigger a hashchange event.
  550. this.redirect(href);
  551. // Prevent default action and further propagation of the event.
  552. return false;
  553. }
  554. // Otherwise alter clicked link's href. This is being picked up by
  555. // the default action handler.
  556. else {
  557. $target
  558. // Restore link's href attribute on blur or next click.
  559. .one('blur mousedown', { target: target, href: target.href }, function (event) { $(event.data.target).attr('href', event.data.href); })
  560. .attr('href', href);
  561. }
  562. }
  563. // Non-admin links should close the overlay and open in the main window,
  564. // which is the default action for a link. We only need to handle them
  565. // if the overlay is open and the clicked link is inside the overlay iframe.
  566. else if (this.isOpen && target.ownerDocument === this.iframeWindow.document) {
  567. // Open external links in the immediate parent of the frame, unless the
  568. // link already has a different target.
  569. if (target.hostname != window.location.hostname) {
  570. if (!$target.attr('target')) {
  571. $target.attr('target', '_parent');
  572. }
  573. }
  574. else {
  575. // Add the overlay-context state to the link, so "overlay-restore" links
  576. // can restore the context.
  577. if ($target[0].hash) {
  578. // Leave links with an existing fragment alone. Adding an extra
  579. // parameter to a link like "node/1#section-1" breaks the link.
  580. }
  581. else {
  582. // For links with no existing fragment, add the overlay context.
  583. $target.attr('href', $.param.fragment(href, { 'overlay-context': this.getPath(window.location) + window.location.search }));
  584. }
  585. // When the link has a destination query parameter and that destination
  586. // is an admin link we need to fragmentize it. This will make it reopen
  587. // in the overlay.
  588. var params = $.deparam.querystring(href);
  589. if (params.destination && this.isAdminLink(params.destination)) {
  590. var fragmentizedDestination = $.param.fragment(this.getPath(window.location), { overlay: params.destination });
  591. $target.attr('href', $.param.querystring(href, { destination: fragmentizedDestination }));
  592. }
  593. // Make the link open in the immediate parent of the frame, unless the
  594. // link already has a different target.
  595. if (!$target.attr('target')) {
  596. $target.attr('target', '_parent');
  597. }
  598. }
  599. }
  600. }
  601. };
  602. /**
  603. * Event handler: opens or closes the overlay based on the current URL fragment.
  604. *
  605. * @param event
  606. * Event being triggered, with the following restrictions:
  607. * - event.type: hashchange
  608. * - event.currentTarget: document
  609. */
  610. Drupal.overlay.eventhandlerOperateByURLFragment = function (event) {
  611. // If we changed the hash to reflect an internal redirect in the overlay,
  612. // its location has already been changed, so don't do anything.
  613. if ($.data(window.location, window.location.href) === 'redirect') {
  614. $.data(window.location, window.location.href, null);
  615. return;
  616. }
  617. // Get the overlay URL from the current URL fragment.
  618. var internalUrl = null;
  619. var state = $.bbq.getState('overlay');
  620. if (state) {
  621. internalUrl = this.getInternalUrl(state);
  622. }
  623. if (internalUrl) {
  624. // Append render variable, so the server side can choose the right
  625. // rendering and add child frame code to the page if needed.
  626. var url = $.param.querystring(internalUrl, { render: 'overlay' });
  627. this.open(url);
  628. this.resetActiveClass(this.getPath(Drupal.settings.basePath + state));
  629. }
  630. // If there is no overlay URL in the fragment and the overlay is (still)
  631. // open, close the overlay.
  632. else if (this.isOpen && !this.isClosing) {
  633. this.close();
  634. this.resetActiveClass(this.getPath(window.location));
  635. }
  636. };
  637. /**
  638. * Event handler: makes sure the internal overlay URL is reflected in the parent
  639. * URL fragment.
  640. *
  641. * Normally the parent URL fragment determines the overlay location. However, if
  642. * the overlay redirects internally, the parent doesn't get informed, and the
  643. * parent URL fragment will be out of date. This is a sanity check to make
  644. * sure we're in the right place.
  645. *
  646. * The parent URL fragment is also not updated automatically when overlay's
  647. * open, close or load functions are used directly (instead of through
  648. * eventhandlerOperateByURLFragment).
  649. *
  650. * @param event
  651. * Event being triggered, with the following restrictions:
  652. * - event.type: drupalOverlayReady, drupalOverlayClose
  653. * - event.currentTarget: document
  654. */
  655. Drupal.overlay.eventhandlerSyncURLFragment = function (event) {
  656. if (this.isOpen) {
  657. var expected = $.bbq.getState('overlay');
  658. // This is just a sanity check, so we're comparing paths, not query strings.
  659. if (this.getPath(Drupal.settings.basePath + expected) != this.getPath(this.iframeWindow.document.location)) {
  660. // There may have been a redirect inside the child overlay window that the
  661. // parent wasn't aware of. Update the parent URL fragment appropriately.
  662. var newLocation = Drupal.overlay.fragmentizeLink(this.iframeWindow.document.location);
  663. // Set a 'redirect' flag on the new location so the hashchange event handler
  664. // knows not to change the overlay's content.
  665. $.data(window.location, newLocation, 'redirect');
  666. // Use location.replace() so we don't create an extra history entry.
  667. window.location.replace(newLocation);
  668. }
  669. }
  670. else {
  671. $.bbq.removeState('overlay');
  672. }
  673. };
  674. /**
  675. * Event handler: if the child window suggested that the parent refresh on
  676. * close, force a page refresh.
  677. *
  678. * @param event
  679. * Event being triggered, with the following restrictions:
  680. * - event.type: drupalOverlayClose
  681. * - event.currentTarget: document
  682. */
  683. Drupal.overlay.eventhandlerRefreshPage = function (event) {
  684. if (Drupal.overlay.refreshPage) {
  685. window.location.reload(true);
  686. }
  687. };
  688. /**
  689. * Event handler: dispatches events to the overlay document.
  690. *
  691. * @param event
  692. * Event being triggered, with the following restrictions:
  693. * - event.type: any
  694. * - event.currentTarget: any
  695. */
  696. Drupal.overlay.eventhandlerDispatchEvent = function (event) {
  697. if (this.iframeWindow && this.iframeWindow.document) {
  698. this.iframeWindow.jQuery(this.iframeWindow.document).trigger(event);
  699. }
  700. };
  701. /**
  702. * Make a regular admin link into a URL that will trigger the overlay to open.
  703. *
  704. * @param link
  705. * A JavaScript Link object (i.e. an <a> element).
  706. * @param parentLocation
  707. * (optional) URL to override the parent window's location with.
  708. *
  709. * @return
  710. * A URL that will trigger the overlay (in the form
  711. * /node/1#overlay=admin/config).
  712. */
  713. Drupal.overlay.fragmentizeLink = function (link, parentLocation) {
  714. // Don't operate on links that are already overlay-ready.
  715. var params = $.deparam.fragment(link.href);
  716. if (params.overlay) {
  717. return link.href;
  718. }
  719. // Determine the link's original destination. Set ignorePathFromQueryString to
  720. // true to prevent transforming this link into a clean URL while clean URLs
  721. // may be disabled.
  722. var path = this.getPath(link, true);
  723. // Preserve existing query and fragment parameters in the URL, except for
  724. // "render=overlay" which is re-added in Drupal.overlay.eventhandlerOperateByURLFragment.
  725. var destination = path + link.search.replace(/&?render=overlay/, '').replace(/\?$/, '') + link.hash;
  726. // Assemble and return the overlay-ready link.
  727. return $.param.fragment(parentLocation || window.location.href, { overlay: destination });
  728. };
  729. /**
  730. * Refresh any regions of the page that are displayed outside the overlay.
  731. *
  732. * @param data
  733. * An array of objects with information on the page regions to be refreshed.
  734. * For each object, the key is a CSS class identifying the region to be
  735. * refreshed, and the value represents the section of the Drupal $page array
  736. * corresponding to this region.
  737. */
  738. Drupal.overlay.refreshRegions = function (data) {
  739. $.each(data, function () {
  740. var region_info = this;
  741. $.each(region_info, function (regionClass) {
  742. var regionName = region_info[regionClass];
  743. var regionSelector = '.' + regionClass;
  744. // Allow special behaviors to detach.
  745. Drupal.detachBehaviors($(regionSelector));
  746. $.get(Drupal.settings.basePath + Drupal.settings.overlay.ajaxCallback + '/' + regionName, function (newElement) {
  747. $(regionSelector).replaceWith($(newElement));
  748. Drupal.attachBehaviors($(regionSelector), Drupal.settings);
  749. });
  750. });
  751. });
  752. };
  753. /**
  754. * Reset the active class on links in displaced elements according to
  755. * given path.
  756. *
  757. * @param activePath
  758. * Path to match links against.
  759. */
  760. Drupal.overlay.resetActiveClass = function(activePath) {
  761. var self = this;
  762. var windowDomain = window.location.protocol + window.location.hostname;
  763. $('.overlay-displace-top, .overlay-displace-bottom')
  764. .find('a[href]')
  765. // Remove active class from all links in displaced elements.
  766. .removeClass('active')
  767. // Add active class to links that match activePath.
  768. .each(function () {
  769. var linkDomain = this.protocol + this.hostname;
  770. var linkPath = self.getPath(this);
  771. // A link matches if it is part of the active trail of activePath, except
  772. // for frontpage links.
  773. if (linkDomain == windowDomain && (activePath + '/').indexOf(linkPath + '/') === 0 && (linkPath !== '' || activePath === '')) {
  774. $(this).addClass('active');
  775. }
  776. });
  777. };
  778. /**
  779. * Helper function to get the (corrected) Drupal path of a link.
  780. *
  781. * @param link
  782. * Link object or string to get the Drupal path from.
  783. * @param ignorePathFromQueryString
  784. * Boolean whether to ignore path from query string if path appears empty.
  785. *
  786. * @return
  787. * The Drupal path.
  788. */
  789. Drupal.overlay.getPath = function (link, ignorePathFromQueryString) {
  790. if (typeof link == 'string') {
  791. // Create a native Link object, so we can use its object methods.
  792. link = $(link.link(link)).get(0);
  793. }
  794. var path = link.pathname;
  795. // Ensure a leading slash on the path, omitted in some browsers.
  796. if (path.charAt(0) != '/') {
  797. path = '/' + path;
  798. }
  799. path = path.replace(new RegExp(Drupal.settings.basePath + '(?:index.php)?'), '');
  800. if (path == '' && !ignorePathFromQueryString) {
  801. // If the path appears empty, it might mean the path is represented in the
  802. // query string (clean URLs are not used).
  803. var match = new RegExp('([?&])q=(.+)([&#]|$)').exec(link.search);
  804. if (match && match.length == 4) {
  805. path = match[2];
  806. }
  807. }
  808. return path;
  809. };
  810. /**
  811. * Get the total displacement of given region.
  812. *
  813. * @param region
  814. * Region name. Either "top" or "bottom".
  815. *
  816. * @return
  817. * The total displacement of given region in pixels.
  818. */
  819. Drupal.overlay.getDisplacement = function (region) {
  820. var displacement = 0;
  821. var lastDisplaced = $('.overlay-displace-' + region + ':last');
  822. if (lastDisplaced.length) {
  823. displacement = lastDisplaced.offset().top + lastDisplaced.outerHeight();
  824. // In modern browsers (including IE9), when box-shadow is defined, use the
  825. // normal height.
  826. var cssBoxShadowValue = lastDisplaced.css('box-shadow');
  827. var boxShadow = (typeof cssBoxShadowValue !== 'undefined' && cssBoxShadowValue !== 'none');
  828. // In IE8 and below, we use the shadow filter to apply box-shadow styles to
  829. // the toolbar. It adds some extra height that we need to remove.
  830. if (!boxShadow && /DXImageTransform\.Microsoft\.Shadow/.test(lastDisplaced.css('filter'))) {
  831. displacement -= lastDisplaced[0].filters.item('DXImageTransform.Microsoft.Shadow').strength;
  832. displacement = Math.max(0, displacement);
  833. }
  834. }
  835. return displacement;
  836. };
  837. /**
  838. * Makes elements outside the overlay unreachable via the tab key.
  839. *
  840. * @param context
  841. * The part of the DOM that should have its tabindexes changed. Defaults to
  842. * the entire page.
  843. */
  844. Drupal.overlay.makeDocumentUntabbable = function (context) {
  845. context = context || document.body;
  846. var $overlay, $tabbable, $hasTabindex;
  847. // Determine which elements on the page already have a tabindex.
  848. $hasTabindex = $('[tabindex] :not(.overlay-element)', context);
  849. // Record the tabindex for each element, so we can restore it later.
  850. $hasTabindex.each(Drupal.overlay._recordTabindex);
  851. // Add the tabbable elements from the current context to any that we might
  852. // have previously recorded.
  853. Drupal.overlay._hasTabindex = $hasTabindex.add(Drupal.overlay._hasTabindex);
  854. // Set tabindex to -1 on everything outside the overlay and toolbars, so that
  855. // the underlying page is unreachable.
  856. // By default, browsers make a, area, button, input, object, select, textarea,
  857. // and iframe elements reachable via the tab key.
  858. $tabbable = $('a, area, button, input, object, select, textarea, iframe');
  859. // If another element (like a div) has a tabindex, it's also tabbable.
  860. $tabbable = $tabbable.add($hasTabindex);
  861. // Leave links inside the overlay and toolbars alone.
  862. $overlay = $('.overlay-element, #overlay-container, .overlay-displace-top, .overlay-displace-bottom').find('*');
  863. $tabbable = $tabbable.not($overlay);
  864. // We now have a list of everything in the underlying document that could
  865. // possibly be reachable via the tab key. Make it all unreachable.
  866. $tabbable.attr('tabindex', -1);
  867. };
  868. /**
  869. * Restores the original tabindex value of a group of elements.
  870. *
  871. * @param context
  872. * The part of the DOM that should have its tabindexes restored. Defaults to
  873. * the entire page.
  874. */
  875. Drupal.overlay.makeDocumentTabbable = function (context) {
  876. var $needsTabindex;
  877. context = context || document.body;
  878. // Make the underlying document tabbable again by removing all existing
  879. // tabindex attributes.
  880. var $tabindex = $('[tabindex]', context);
  881. $tabindex.removeAttr('tabindex');
  882. // Restore the tabindex attributes that existed before the overlay was opened.
  883. $needsTabindex = $(Drupal.overlay._hasTabindex, context);
  884. $needsTabindex.each(Drupal.overlay._restoreTabindex);
  885. Drupal.overlay._hasTabindex = Drupal.overlay._hasTabindex.not($needsTabindex);
  886. };
  887. /**
  888. * Record the tabindex for an element, using $.data.
  889. *
  890. * Meant to be used as a jQuery.fn.each callback.
  891. */
  892. Drupal.overlay._recordTabindex = function () {
  893. var $element = $(this);
  894. var tabindex = $(this).attr('tabindex');
  895. $element.data('drupalOverlayOriginalTabIndex', tabindex);
  896. };
  897. /**
  898. * Restore an element's original tabindex.
  899. *
  900. * Meant to be used as a jQuery.fn.each callback.
  901. */
  902. Drupal.overlay._restoreTabindex = function () {
  903. var $element = $(this);
  904. var tabindex = $element.data('drupalOverlayOriginalTabIndex');
  905. $element.attr('tabindex', tabindex);
  906. };
  907. /**
  908. * Theme function to create the overlay iframe element.
  909. */
  910. Drupal.theme.prototype.overlayContainer = function () {
  911. return '<div id="overlay-container"><div class="overlay-modal-background"></div></div>';
  912. };
  913. /**
  914. * Theme function to create an overlay iframe element.
  915. */
  916. Drupal.theme.prototype.overlayElement = function (url) {
  917. return '<iframe class="overlay-element" frameborder="0" scrolling="auto" allowtransparency="true"></iframe>';
  918. };
  919. })(jQuery);