Newer
Older
Jennifer Hodgdon
committed
/**
* @file
* Attaches the behaviors for the Overlay parent pages.
*/
(function ($) {
/**
* Open the overlay, or load content into it, when an admin link is clicked.
*/
Drupal.behaviors.overlayParent = {
attach: function (context, settings) {
Dries Buytaert
committed
if (Drupal.overlay.isOpen) {
Drupal.overlay.makeDocumentUntabbable(context);
}
if (this.processed) {
return;
}
this.processed = true;
Angie Byron
committed
$(window)
Angie Byron
committed
// When the hash (URL fragment) changes, open the overlay if needed.
.bind('hashchange.drupal-overlay', $.proxy(Drupal.overlay, 'eventhandlerOperateByURLFragment'))
// Trigger the hashchange handler once, after the page is loaded, so that
// permalinks open the overlay.
.triggerHandler('hashchange.drupal-overlay');
$(document)
// Instead of binding a click event handler to every link we bind one to
// the document and only handle events that bubble up. This allows other
// scripts to bind their own handlers to links and also to prevent
// overlay's handling.
.bind('click.drupal-overlay mouseup.drupal-overlay', $.proxy(Drupal.overlay, 'eventhandlerOverrideLink'));
}
};
/**
* Overlay object for parent windows.
Dries Buytaert
committed
*
* Events
* Overlay triggers a number of events that can be used by other scripts.
* - drupalOverlayOpen: This event is triggered when the overlay is opened.
* - drupalOverlayBeforeClose: This event is triggered when the overlay attempts
* to close. If an event handler returns false, the close will be prevented.
* - drupalOverlayClose: This event is triggered when the overlay is closed.
* - drupalOverlayBeforeLoad: This event is triggered right before a new URL
* is loaded into the overlay.
Angie Byron
committed
* - drupalOverlayReady: This event is triggered when the DOM of the overlay
* child document is fully loaded.
Dries Buytaert
committed
* - drupalOverlayLoad: This event is triggered when the overlay is finished
* loading.
Angie Byron
committed
* - drupalOverlayResize: This event is triggered when the overlay is being
* resized to match the parent window.
*/
Drupal.overlay = Drupal.overlay || {
Angie Byron
committed
isOpen: false,
isOpening: false,
isClosing: false,
Angie Byron
committed
isLoading: false
};
Angie Byron
committed
Drupal.overlay.prototype = {};
/**
Angie Byron
committed
* Open the overlay.
Angie Byron
committed
* @param url
* The URL of the page to open in the overlay.
*
* @return
Angie Byron
committed
* TRUE if the overlay was opened, FALSE otherwise.
*/
Angie Byron
committed
Drupal.overlay.open = function (url) {
// Just one overlay is allowed.
Angie Byron
committed
if (this.isOpen || this.isOpening) {
return this.load(url);
Angie Byron
committed
this.isOpening = true;
Dries Buytaert
committed
// Store the original document title.
this.originalTitle = document.title;
// Create the dialog and related DOM elements.
Angie Byron
committed
this.create();
Angie Byron
committed
this.isOpening = false;
this.isOpen = true;
$(document.documentElement).addClass('overlay-open');
Dries Buytaert
committed
this.makeDocumentUntabbable();
Dries Buytaert
committed
// Allow other scripts to respond to this event.
$(document).trigger('drupalOverlayOpen');
Angie Byron
committed
return this.load(url);
};
/**
* Create the underlying markup and behaviors for the overlay.
*/
Drupal.overlay.create = function () {
Angie Byron
committed
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
this.$container = $(Drupal.theme('overlayContainer'))
.appendTo(document.body);
// Overlay uses transparent iframes that cover the full parent window.
// When the overlay is open the scrollbar of the parent window is hidden.
// Because some browsers show a white iframe background for a short moment
// while loading a page into an iframe, overlay uses two iframes. By loading
// the page in a hidden (inactive) iframe the user doesn't see the white
// background. When the page is loaded the active and inactive iframes
// are switched.
this.activeFrame = this.$iframeA = $(Drupal.theme('overlayElement'))
.appendTo(this.$container);
this.inactiveFrame = this.$iframeB = $(Drupal.theme('overlayElement'))
.appendTo(this.$container);
this.$iframeA.bind('load.drupal-overlay', { self: this.$iframeA[0], sibling: this.$iframeB }, $.proxy(this, 'loadChild'));
this.$iframeB.bind('load.drupal-overlay', { self: this.$iframeB[0], sibling: this.$iframeA }, $.proxy(this, 'loadChild'));
// Add a second class "drupal-overlay-open" to indicate these event handlers
// should only be bound when the overlay is open.
var eventClass = '.drupal-overlay.drupal-overlay-open';
$(window)
.bind('resize' + eventClass, $.proxy(this, 'eventhandlerOuterResize'));
$(document)
.bind('drupalOverlayLoad' + eventClass, $.proxy(this, 'eventhandlerOuterResize'))
.bind('drupalOverlayReady' + eventClass +
' drupalOverlayClose' + eventClass, $.proxy(this, 'eventhandlerSyncURLFragment'))
Dries Buytaert
committed
.bind('drupalOverlayClose' + eventClass, $.proxy(this, 'eventhandlerRefreshPage'))
Angie Byron
committed
.bind('drupalOverlayBeforeClose' + eventClass +
' drupalOverlayBeforeLoad' + eventClass +
Dries Buytaert
committed
' drupalOverlayResize' + eventClass, $.proxy(this, 'eventhandlerDispatchEvent'));
Angie Byron
committed
if ($('.overlay-displace-top, .overlay-displace-bottom').length) {
$(document)
.bind('drupalOverlayResize' + eventClass, $.proxy(this, 'eventhandlerAlterDisplacedElements'))
.bind('drupalOverlayClose' + eventClass, $.proxy(this, 'eventhandlerRestoreDisplacedElements'));
}
};
/**
* Load the given URL into the overlay iframe.
*
* Use this method to change the URL being loaded in the overlay if it is
* already open.
Angie Byron
committed
*
* @return
* TRUE if URL is loaded into the overlay, FALSE otherwise.
*/
Drupal.overlay.load = function (url) {
Angie Byron
committed
if (!this.isOpen) {
return false;
Angie Byron
committed
Dries Buytaert
committed
// Allow other scripts to respond to this event.
$(document).trigger('drupalOverlayBeforeLoad');
Angie Byron
committed
$(document.documentElement).addClass('overlay-loading');
Angie Byron
committed
Angie Byron
committed
// The contentDocument property is not supported in IE until IE8.
var iframeDocument = this.inactiveFrame[0].contentDocument || this.inactiveFrame[0].contentWindow.document;
Angie Byron
committed
// location.replace doesn't create a history entry. location.href does.
// In this case, we want location.replace, as we're creating the history
// entry using URL fragments.
Angie Byron
committed
iframeDocument.location.replace(url);
Angie Byron
committed
return true;
};
/**
Angie Byron
committed
* Close the overlay and remove markup related to it from the document.
Angie Byron
committed
*
* @return
* TRUE if the overlay was closed, FALSE otherwise.
*/
Dries Buytaert
committed
Drupal.overlay.close = function () {
Angie Byron
committed
// Prevent double execution when close is requested more than once.
if (!this.isOpen || this.isClosing) {
return false;
}
// Allow other scripts to respond to this event.
var event = $.Event('drupalOverlayBeforeClose');
$(document).trigger(event);
// If a handler returned false, the close will be prevented.
if (event.isDefaultPrevented()) {
return false;
}
this.isClosing = true;
this.isOpen = false;
$(document.documentElement).removeClass('overlay-open');
Dries Buytaert
committed
// Restore the original document title.
document.title = this.originalTitle;
Dries Buytaert
committed
this.makeDocumentTabbable();
Angie Byron
committed
// Allow other scripts to respond to this event.
$(document).trigger('drupalOverlayClose');
// When the iframe is still loading don't destroy it immediately but after
// the content is loaded (see Drupal.overlay.loadChild).
if (!this.isLoading) {
this.destroy();
this.isClosing = false;
}
return true;
};
/**
Angie Byron
committed
* Destroy the overlay.
*/
Angie Byron
committed
Drupal.overlay.destroy = function () {
Angie Byron
committed
$([document, window]).unbind('.drupal-overlay-open');
this.$container.remove();
Angie Byron
committed
this.$container = null;
this.$iframeA = null;
this.$iframeB = null;
Angie Byron
committed
this.iframeWindow = null;
};
/**
* Redirect the overlay parent window to the given URL.
*
Angie Byron
committed
* @param url
* Can be an absolute URL or a relative link to the domain root.
*/
Angie Byron
committed
Drupal.overlay.redirect = function (url) {
// Create a native Link object, so we can use its object methods.
var link = $(url.link(url)).get(0);
// If the link is already open, force the hashchange event to simulate reload.
Angie Byron
committed
if (window.location.href == link.href) {
$(window).triggerHandler('hashchange.drupal-overlay');
}
Angie Byron
committed
window.location.href = link.href;
return true;
Angie Byron
committed
};
/**
* Bind the child window.
*
Angie Byron
committed
* Note that this function is fired earlier than Drupal.overlay.loadChild.
*/
Angie Byron
committed
Drupal.overlay.bindChild = function (iframeWindow, isClosing) {
Angie Byron
committed
this.iframeWindow = iframeWindow;
// We are done if the child window is closing.
Angie Byron
committed
if (isClosing || this.isClosing || !this.isOpen) {
return;
}
Angie Byron
committed
// Allow other scripts to respond to this event.
$(document).trigger('drupalOverlayReady');
};
Angie Byron
committed
/**
* Event handler: load event handler for the overlay iframe.
*
* @param event
* Event being triggered, with the following restrictions:
* - event.type: load
* - event.currentTarget: iframe
*/
Drupal.overlay.loadChild = function (event) {
var iframe = event.data.self;
var iframeDocument = iframe.contentDocument || iframe.contentWindow.document;
var iframeWindow = iframeDocument.defaultView || iframeDocument.parentWindow;
if (iframeWindow.location == 'about:blank') {
return;
}
Angie Byron
committed
this.isLoading = false;
$(document.documentElement).removeClass('overlay-loading');
Dries Buytaert
committed
event.data.sibling.removeClass('overlay-active').attr({ 'tabindex': -1 });
Angie Byron
committed
// Only continue when overlay is still open and not closing.
if (this.isOpen && !this.isClosing) {
// And child document is an actual overlayChild.
if (iframeWindow.Drupal && iframeWindow.Drupal.overlayChild) {
Dries Buytaert
committed
// Replace the document title with title of iframe.
document.title = iframeWindow.document.title;
Angie Byron
committed
this.activeFrame = $(iframe)
.addClass('overlay-active')
// Add a title attribute to the iframe for accessibility.
Dries Buytaert
committed
.attr('title', Drupal.t('@title dialog', { '@title': iframeWindow.jQuery('#overlay-title').text() })).removeAttr('tabindex');
Angie Byron
committed
this.inactiveFrame = event.data.sibling;
// Load an empty document into the inactive iframe.
(this.inactiveFrame[0].contentDocument || this.inactiveFrame[0].contentWindow.document).location.replace('about:blank');
Dries Buytaert
committed
// Move the focus to just before the "skip to main content" link inside
// the overlay.
this.activeFrame.focus();
var skipLink = iframeWindow.jQuery('a:first');
Drupal.overlay.setFocusBefore(skipLink, iframeWindow.document);
Angie Byron
committed
// Allow other scripts to respond to this event.
$(document).trigger('drupalOverlayLoad');
}
else {
window.location = iframeWindow.location.href.replace(/([?&]?)render=overlay&?/g, '$1').replace(/\?$/, '');
}
Angie Byron
committed
}
Angie Byron
committed
else {
this.destroy();
};
Dries Buytaert
committed
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
/**
* Creates a placeholder element to receive document focus.
*
* Setting the document focus to a link will make it visible, even if it's a
* "skip to main content" link that should normally be visible only when the
* user tabs to it. This function can be used to set the document focus to
* just before such an invisible link.
*
* @param $element
* The jQuery element that should receive focus on the next tab press.
* @param document
* The iframe window element to which the placeholder should be added. The
* placeholder element has to be created inside the same iframe as the element
* it precedes, to keep IE happy. (http://bugs.jquery.com/ticket/4059)
*/
Drupal.overlay.setFocusBefore = function ($element, document) {
// Create an anchor inside the placeholder document.
var placeholder = document.createElement('a');
var $placeholder = $(placeholder).addClass('element-invisible').attr('href', '#');
// Put the placeholder where it belongs, and set the document focus to it.
$placeholder.insertBefore($element);
$placeholder.focus();
// Make the placeholder disappear as soon as it loses focus, so that it
// doesn't appear in the tab order again.
$placeholder.one('blur', function () {
$(this).remove();
});
Dries Buytaert
committed
};
Dries Buytaert
committed
/**
* Check if the given link is in the administrative section of the site.
*
* @param url
Jennifer Hodgdon
committed
* The URL to be tested.
Angie Byron
committed
*
* @return boolean
* TRUE if the URL represents an administrative link, FALSE otherwise.
*/
Drupal.overlay.isAdminLink = function (url) {
Dries Buytaert
committed
if (Drupal.overlay.isExternalLink(url)) {
return false;
}
Angie Byron
committed
var path = this.getPath(url);
// Turn the list of administrative paths into a regular expression.
Angie Byron
committed
if (!this.adminPathRegExp) {
Angie Byron
committed
var prefix = '';
if (Drupal.settings.overlay.pathPrefixes.length) {
// Allow path prefixes used for language negatiation followed by slash,
// and the empty string.
prefix = '(' + Drupal.settings.overlay.pathPrefixes.join('/|') + '/|)';
}
var adminPaths = '^' + prefix + '(' + Drupal.settings.overlay.paths.admin.replace(/\s+/g, '|') + ')$';
var nonAdminPaths = '^' + prefix + '(' + Drupal.settings.overlay.paths.non_admin.replace(/\s+/g, '|') + ')$';
adminPaths = adminPaths.replace(/\*/g, '.*');
nonAdminPaths = nonAdminPaths.replace(/\*/g, '.*');
Angie Byron
committed
this.adminPathRegExp = new RegExp(adminPaths);
this.nonAdminPathRegExp = new RegExp(nonAdminPaths);
}
Angie Byron
committed
return this.adminPathRegExp.exec(path) && !this.nonAdminPathRegExp.exec(path);
Angie Byron
committed
};
Dries Buytaert
committed
/**
* Determine whether a link is external to the site.
*
* @param url
Jennifer Hodgdon
committed
* The URL to be tested.
Dries Buytaert
committed
*
* @return boolean
* TRUE if the URL is external to the site, FALSE otherwise.
*/
Drupal.overlay.isExternalLink = function (url) {
var re = RegExp('^((f|ht)tps?:)?//(?!' + window.location.host + ')');
return re.test(url);
};
/**
Angie Byron
committed
* Event handler: resizes overlay according to the size of the parent window.
*
Angie Byron
committed
* @param event
* Event being triggered, with the following restrictions:
* - event.type: any
* - event.currentTarget: any
*/
Angie Byron
committed
Drupal.overlay.eventhandlerOuterResize = function (event) {
// Proceed only if the overlay still exists.
if (!(this.isOpen || this.isOpening) || this.isClosing || !this.iframeWindow) {
Angie Byron
committed
return;
}
Angie Byron
committed
// IE6 uses position:absolute instead of position:fixed.
if (typeof document.body.style.maxHeight != 'string') {
this.activeFrame.height($(window).height());
Dries Buytaert
committed
Angie Byron
committed
// Allow other scripts to respond to this event.
$(document).trigger('drupalOverlayResize');
};
/**
Angie Byron
committed
* Event handler: resizes displaced elements so they won't overlap the scrollbar
* of overlay's iframe.
*
* @param event
* Event being triggered, with the following restrictions:
* - event.type: any
* - event.currentTarget: any
*/
Angie Byron
committed
Drupal.overlay.eventhandlerAlterDisplacedElements = function (event) {
// Proceed only if the overlay still exists.
if (!(this.isOpen || this.isOpening) || this.isClosing || !this.iframeWindow) {
Angie Byron
committed
return;
}
Angie Byron
committed
$(this.iframeWindow.document.body).css({
marginTop: Drupal.overlay.getDisplacement('top'),
marginBottom: Drupal.overlay.getDisplacement('bottom')
})
// IE7 isn't reflowing the document immediately.
// @todo This might be fixed in a cleaner way.
.addClass('overlay-trigger-reflow').removeClass('overlay-trigger-reflow');
var documentHeight = this.iframeWindow.document.body.clientHeight;
var documentWidth = this.iframeWindow.document.body.clientWidth;
// IE6 doesn't support maxWidth, use width instead.
var maxWidthName = (typeof document.body.style.maxWidth == 'string') ? 'maxWidth' : 'width';
Angie Byron
committed
if (Drupal.overlay.leftSidedScrollbarOffset === undefined && $(document.documentElement).attr('dir') === 'rtl') {
// We can't use element.clientLeft to detect whether scrollbars are placed
// on the left side of the element when direction is set to "rtl" as most
// browsers dont't support it correctly.
// http://www.gtalbot.org/BugzillaSection/DocumentAllDHTMLproperties.html
// There seems to be absolutely no way to detect whether the scrollbar
// is on the left side in Opera; always expect scrollbar to be on the left.
if ($.browser.opera) {
Drupal.overlay.leftSidedScrollbarOffset = document.documentElement.clientWidth - this.iframeWindow.document.documentElement.clientWidth + this.iframeWindow.document.documentElement.clientLeft;
}
else if (this.iframeWindow.document.documentElement.clientLeft) {
Drupal.overlay.leftSidedScrollbarOffset = this.iframeWindow.document.documentElement.clientLeft;
}
else {
var el1 = $('<div style="direction: rtl; overflow: scroll;"></div>').appendTo(document.body);
var el2 = $('<div></div>').appendTo(el1);
Drupal.overlay.leftSidedScrollbarOffset = parseInt(el2[0].offsetLeft - el1[0].offsetLeft);
el1.remove();
}
}
Angie Byron
committed
// Consider any element that should be visible above the overlay (such as
// a toolbar).
$('.overlay-displace-top, .overlay-displace-bottom').each(function () {
var data = $(this).data();
var maxWidth = documentWidth;
// In IE, Shadow filter makes element to overlap the scrollbar with 1px.
if (this.filters && this.filters.length && this.filters.item('DXImageTransform.Microsoft.Shadow')) {
maxWidth -= 1;
}
Angie Byron
committed
if (Drupal.overlay.leftSidedScrollbarOffset) {
$(this).css('left', Drupal.overlay.leftSidedScrollbarOffset);
}
Angie Byron
committed
// Prevent displaced elements overlapping window's scrollbar.
var currentMaxWidth = parseInt($(this).css(maxWidthName));
if ((data.drupalOverlay && data.drupalOverlay.maxWidth) || isNaN(currentMaxWidth) || currentMaxWidth > maxWidth || currentMaxWidth <= 0) {
$(this).css(maxWidthName, maxWidth);
(data.drupalOverlay = data.drupalOverlay || {}).maxWidth = true;
Angie Byron
committed
}
Angie Byron
committed
// Use a more rigorous approach if the displaced element still overlaps
Angie Byron
committed
// window's scrollbar; clip the element on the right.
var offset = $(this).offset();
var offsetRight = offset.left + $(this).outerWidth();
if ((data.drupalOverlay && data.drupalOverlay.clip) || offsetRight > maxWidth) {
Angie Byron
committed
if (Drupal.overlay.leftSidedScrollbarOffset) {
$(this).css('clip', 'rect(auto, auto, ' + (documentHeight - offset.top) + 'px, ' + (Drupal.overlay.leftSidedScrollbarOffset + 2) + 'px)');
}
else {
$(this).css('clip', 'rect(auto, ' + (maxWidth - offset.left) + 'px, ' + (documentHeight - offset.top) + 'px, auto)');
}
Angie Byron
committed
(data.drupalOverlay = data.drupalOverlay || {}).clip = true;
Angie Byron
committed
}
Angie Byron
committed
});
};
Angie Byron
committed
/**
* Event handler: restores size of displaced elements as they were before
Angie Byron
committed
* overlay was opened.
*
* @param event
* Event being triggered, with the following restrictions:
* - event.type: any
* - event.currentTarget: any
*/
Drupal.overlay.eventhandlerRestoreDisplacedElements = function (event) {
var $displacedElements = $('.overlay-displace-top, .overlay-displace-bottom');
try {
$displacedElements.css({ maxWidth: '', clip: '' });
Angie Byron
committed
}
Angie Byron
committed
// IE bug that doesn't allow unsetting style.clip (http://dev.jquery.com/ticket/6512).
catch (err) {
$displacedElements.attr('style', function (index, attr) {
return attr.replace(/clip\s*:\s*rect\([^)]+\);?/i, '');
});
Angie Byron
committed
}
};
/**
Angie Byron
committed
* Event handler: overrides href of administrative links to be opened in
* the overlay.
Angie Byron
committed
*
Angie Byron
committed
* This click event handler should be bound to any document (for example the
Angie Byron
committed
* overlay iframe) of which you want links to open in the overlay.
*
Angie Byron
committed
* @param event
* Event being triggered, with the following restrictions:
* - event.type: click, mouseup
* - event.currentTarget: document
*
Angie Byron
committed
* @see Drupal.overlayChild.behaviors.addClickHandler
*/
Angie Byron
committed
Drupal.overlay.eventhandlerOverrideLink = function (event) {
// In some browsers the click event isn't fired for right-clicks. Use the
// mouseup event for right-clicks and the click event for everything else.
if ((event.type == 'click' && event.button == 2) || (event.type == 'mouseup' && event.button != 2)) {
Angie Byron
committed
return;
}
Angie Byron
committed
var $target = $(event.target);
// Only continue if clicked target (or one of its parents) is a link.
Angie Byron
committed
if (!$target.is('a')) {
Angie Byron
committed
$target = $target.closest('a');
if (!$target.length) {
return;
}
}
Angie Byron
committed
// Never open links in the overlay that contain the overlay-exclude class.
if ($target.hasClass('overlay-exclude')) {
return;
}
Angie Byron
committed
// Close the overlay when the link contains the overlay-close class.
if ($target.hasClass('overlay-close')) {
// Clearing the overlay URL fragment will close the overlay.
Dries Buytaert
committed
$.bbq.removeState('overlay');
Angie Byron
committed
return;
}
var target = $target[0];
var href = target.href;
Jennifer Hodgdon
committed
// Only handle links that have an href attribute and use the HTTP(S) protocol.
Angie Byron
committed
if (href != undefined && href != '' && target.protocol.match(/^https?\:/)) {
var anchor = href.replace(target.ownerDocument.location.href, '');
// Skip anchor links.
if (anchor.length == 0 || anchor.charAt(0) == '#') {
return;
}
Angie Byron
committed
// Open admin links in the overlay.
Angie Byron
committed
else if (this.isAdminLink(href)) {
Angie Byron
committed
// If the link contains the overlay-restore class and the overlay-context
// state is set, also update the parent window's location.
var parentLocation = ($target.hasClass('overlay-restore') && typeof $.bbq.getState('overlay-context') == 'string')
? Drupal.settings.basePath + $.bbq.getState('overlay-context')
: null;
href = this.fragmentizeLink($target.get(0), parentLocation);
Angie Byron
committed
// Only override default behavior when left-clicking and user is not
// pressing the ALT, CTRL, META (Command key on the Macintosh keyboard)
// or SHIFT key.
if (event.button == 0 && !event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey) {
// Redirect to a fragmentized href. This will trigger a hashchange event.
Angie Byron
committed
this.redirect(href);
Angie Byron
committed
// Prevent default action and further propagation of the event.
return false;
}
Angie Byron
committed
// Otherwise alter clicked link's href. This is being picked up by
Angie Byron
committed
// the default action handler.
else {
Angie Byron
committed
$target
// Restore link's href attribute on blur or next click.
.one('blur mousedown', { target: target, href: target.href }, function (event) { $(event.data.target).attr('href', event.data.href); })
.attr('href', href);
Angie Byron
committed
}
}
Dries Buytaert
committed
// Non-admin links should close the overlay and open in the main window,
// which is the default action for a link. We only need to handle them
// if the overlay is open and the clicked link is inside the overlay iframe.
Angie Byron
committed
else if (this.isOpen && target.ownerDocument === this.iframeWindow.document) {
Dries Buytaert
committed
// Open external links in the immediate parent of the frame, unless the
// link already has a different target.
Dries Buytaert
committed
if (target.hostname != window.location.hostname) {
if (!$target.attr('target')) {
Dries Buytaert
committed
$target.attr('target', '_parent');
Dries Buytaert
committed
}
}
else {
Angie Byron
committed
// Add the overlay-context state to the link, so "overlay-restore" links
// can restore the context.
Angie Byron
committed
if ($target[0].hash) {
// Leave links with an existing fragment alone. Adding an extra
// parameter to a link like "node/1#section-1" breaks the link.
}
else {
// For links with no existing fragment, add the overlay context.
$target.attr('href', $.param.fragment(href, { 'overlay-context': this.getPath(window.location) + window.location.search }));
}
Angie Byron
committed
Dries Buytaert
committed
// When the link has a destination query parameter and that destination
// is an admin link we need to fragmentize it. This will make it reopen
// in the overlay.
var params = $.deparam.querystring(href);
if (params.destination && this.isAdminLink(params.destination)) {
var fragmentizedDestination = $.param.fragment(this.getPath(window.location), { overlay: params.destination });
$target.attr('href', $.param.querystring(href, { destination: fragmentizedDestination }));
}
David Rothstein
committed
// Make the link open in the immediate parent of the frame, unless the
// link already has a different target.
if (!$target.attr('target')) {
$target.attr('target', '_parent');
}
Angie Byron
committed
}
}
}
};
/**
Angie Byron
committed
* Event handler: opens or closes the overlay based on the current URL fragment.
*
* @param event
* Event being triggered, with the following restrictions:
* - event.type: hashchange
* - event.currentTarget: document
*/
Angie Byron
committed
Drupal.overlay.eventhandlerOperateByURLFragment = function (event) {
Angie Byron
committed
// If we changed the hash to reflect an internal redirect in the overlay,
// its location has already been changed, so don't do anything.
if ($.data(window.location, window.location.href) === 'redirect') {
$.data(window.location, window.location.href, null);
return;
}
// Get the overlay URL from the current URL fragment.
var state = $.bbq.getState('overlay');
if (state) {
// Append render variable, so the server side can choose the right
Angie Byron
committed
// rendering and add child frame code to the page if needed.
var url = $.param.querystring(Drupal.settings.basePath + state, { render: 'overlay' });
Angie Byron
committed
Angie Byron
committed
this.open(url);
Dries Buytaert
committed
this.resetActiveClass(this.getPath(Drupal.settings.basePath + state));
Angie Byron
committed
}
// If there is no overlay URL in the fragment and the overlay is (still)
// open, close the overlay.
else if (this.isOpen && !this.isClosing) {
this.close();
this.resetActiveClass(this.getPath(window.location));
}
};
Angie Byron
committed
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
/**
* Event handler: makes sure the internal overlay URL is reflected in the parent
* URL fragment.
*
* Normally the parent URL fragment determines the overlay location. However, if
* the overlay redirects internally, the parent doesn't get informed, and the
* parent URL fragment will be out of date. This is a sanity check to make
* sure we're in the right place.
*
* The parent URL fragment is also not updated automatically when overlay's
* open, close or load functions are used directly (instead of through
* eventhandlerOperateByURLFragment).
*
* @param event
* Event being triggered, with the following restrictions:
* - event.type: drupalOverlayReady, drupalOverlayClose
* - event.currentTarget: document
*/
Drupal.overlay.eventhandlerSyncURLFragment = function (event) {
if (this.isOpen) {
var expected = $.bbq.getState('overlay');
// This is just a sanity check, so we're comparing paths, not query strings.
if (this.getPath(Drupal.settings.basePath + expected) != this.getPath(this.iframeWindow.document.location)) {
// There may have been a redirect inside the child overlay window that the
// parent wasn't aware of. Update the parent URL fragment appropriately.
var newLocation = Drupal.overlay.fragmentizeLink(this.iframeWindow.document.location);
// Set a 'redirect' flag on the new location so the hashchange event handler
// knows not to change the overlay's content.
$.data(window.location, newLocation, 'redirect');
// Use location.replace() so we don't create an extra history entry.
window.location.replace(newLocation);
}
Angie Byron
committed
}
else {
Dries Buytaert
committed
$.bbq.removeState('overlay');
}
};
/**
* Event handler: if the child window suggested that the parent refresh on
* close, force a page refresh.
*
* @param event
* Event being triggered, with the following restrictions:
* - event.type: drupalOverlayClose
* - event.currentTarget: document
*/
Drupal.overlay.eventhandlerRefreshPage = function (event) {
if (Drupal.overlay.refreshPage) {
window.location.reload(true);
Angie Byron
committed
}
};
/**
* Event handler: dispatches events to the overlay document.
*
* @param event
* Event being triggered, with the following restrictions:
* - event.type: any
* - event.currentTarget: any
*/
Drupal.overlay.eventhandlerDispatchEvent = function (event) {
if (this.iframeWindow && this.iframeWindow.document) {
this.iframeWindow.jQuery(this.iframeWindow.document).trigger(event);
}
};
/**
* Make a regular admin link into a URL that will trigger the overlay to open.
*
* @param link
* A JavaScript Link object (i.e. an <a> element).
Angie Byron
committed
* @param parentLocation
* (optional) URL to override the parent window's location with.
Angie Byron
committed
*
* @return
* A URL that will trigger the overlay (in the form
* /node/1#overlay=admin/config).
*/
Angie Byron
committed
Drupal.overlay.fragmentizeLink = function (link, parentLocation) {
// Don't operate on links that are already overlay-ready.
var params = $.deparam.fragment(link.href);
if (params.overlay) {
return link.href;
}
Angie Byron
committed
// Determine the link's original destination. Set ignorePathFromQueryString to
// true to prevent transforming this link into a clean URL while clean URLs
// may be disabled.
Angie Byron
committed
var path = this.getPath(link, true);
Dries Buytaert
committed
// Preserve existing query and fragment parameters in the URL, except for
Angie Byron
committed
// "render=overlay" which is re-added in Drupal.overlay.eventhandlerOperateByURLFragment.
Dries Buytaert
committed
var destination = path + link.search.replace(/&?render=overlay/, '').replace(/\?$/, '') + link.hash;
// Assemble and return the overlay-ready link.
Angie Byron
committed
return $.param.fragment(parentLocation || window.location.href, { overlay: destination });
Angie Byron
committed
};
/**
* Refresh any regions of the page that are displayed outside the overlay.
*
* @param data
* An array of objects with information on the page regions to be refreshed.
* For each object, the key is a CSS class identifying the region to be
* refreshed, and the value represents the section of the Drupal $page array
* corresponding to this region.
*/
Drupal.overlay.refreshRegions = function (data) {
$.each(data, function () {
var region_info = this;
$.each(region_info, function (regionClass) {
var regionName = region_info[regionClass];
var regionSelector = '.' + regionClass;
Angie Byron
committed
// Allow special behaviors to detach.
Drupal.detachBehaviors($(regionSelector));
$.get(Drupal.settings.basePath + Drupal.settings.overlay.ajaxCallback + '/' + regionName, function (newElement) {
$(regionSelector).replaceWith($(newElement));
Drupal.attachBehaviors($(regionSelector), Drupal.settings);
});
});
});
};
Angie Byron
committed
/**
Angie Byron
committed
* Reset the active class on links in displaced elements according to
* given path.
Angie Byron
committed
*
* @param activePath
* Path to match links against.
*/
Drupal.overlay.resetActiveClass = function(activePath) {
var self = this;
Dries Buytaert
committed
var windowDomain = window.location.protocol + window.location.hostname;
Angie Byron
committed
$('.overlay-displace-top, .overlay-displace-bottom')
Angie Byron
committed
.find('a[href]')
Angie Byron
committed
// Remove active class from all links in displaced elements.
Angie Byron
committed
.removeClass('active')
// Add active class to links that match activePath.
.each(function () {
var linkDomain = this.protocol + this.hostname;
var linkPath = self.getPath(this);
Dries Buytaert
committed
// A link matches if it is part of the active trail of activePath, except
// for frontpage links.
if (linkDomain == windowDomain && (activePath + '/').indexOf(linkPath + '/') === 0 && (linkPath !== '' || activePath === '')) {
Angie Byron
committed
$(this).addClass('active');
}
});
};
/**
* Helper function to get the (corrected) Drupal path of a link.
*
* @param link
* Link object or string to get the Drupal path from.
* @param ignorePathFromQueryString
* Boolean whether to ignore path from query string if path appears empty.
Angie Byron
committed
*
Angie Byron
committed
* @return
Angie Byron
committed
* The Drupal path.
Angie Byron
committed
*/
Drupal.overlay.getPath = function (link, ignorePathFromQueryString) {
if (typeof link == 'string') {
// Create a native Link object, so we can use its object methods.
link = $(link.link(link)).get(0);
}
var path = link.pathname;
// Ensure a leading slash on the path, omitted in some browsers.
if (path.charAt(0) != '/') {
path = '/' + path;
}
Angie Byron
committed
path = path.replace(new RegExp(Drupal.settings.basePath + '(?:index.php)?'), '');
Angie Byron
committed
if (path == '' && !ignorePathFromQueryString) {
// If the path appears empty, it might mean the path is represented in the
// query string (clean URLs are not used).
Angie Byron
committed
var match = new RegExp('([?&])q=(.+)([&#]|$)').exec(link.search);
Angie Byron
committed
if (match && match.length == 4) {
path = match[2];
}
}
return path;
};
/**
Angie Byron
committed
* Get the total displacement of given region.
*
* @param region
* Region name. Either "top" or "bottom".
*
* @return
* The total displacement of given region in pixels.
*/
Angie Byron
committed
Drupal.overlay.getDisplacement = function (region) {
var displacement = 0;
var lastDisplaced = $('.overlay-displace-' + region + ':last');
if (lastDisplaced.length) {
displacement = lastDisplaced.offset().top + lastDisplaced.outerHeight();
Angie Byron
committed
// In modern browsers (including IE9), when box-shadow is defined, use the
// normal height.
var cssBoxShadowValue = lastDisplaced.css('box-shadow');
var boxShadow = (typeof cssBoxShadowValue !== 'undefined' && cssBoxShadowValue !== 'none');
// In IE8 and below, we use the shadow filter to apply box-shadow styles to
// the toolbar. It adds some extra height that we need to remove.
if (!boxShadow && /DXImageTransform\.Microsoft\.Shadow/.test(lastDisplaced.css('filter'))) {
Angie Byron
committed
displacement -= lastDisplaced[0].filters.item('DXImageTransform.Microsoft.Shadow').strength;
displacement = Math.max(0, displacement);
}
}
return displacement;
};
Dries Buytaert
committed
/**
* Makes elements outside the overlay unreachable via the tab key.
*
* @param context
* The part of the DOM that should have its tabindexes changed. Defaults to
* the entire page.
*/
Drupal.overlay.makeDocumentUntabbable = function (context) {
Dries Buytaert
committed
// Manipulating tabindexes for the entire document is unacceptably slow in IE6
// and IE7, so in those browsers, the underlying page will still be reachable
// via the tab key. However, we still make the links within the Disable
// message unreachable, because the same message also exists within the
// child document. The duplicate copy in the underlying document is only for
// assisting screen-reader users navigating the document with reading commands
// that follow markup order rather than tab order.
Dries Buytaert
committed
if (jQuery.browser.msie && parseInt(jQuery.browser.version, 10) < 8) {
Dries Buytaert
committed
$('#overlay-disable-message a', context).attr('tabindex', -1);
Dries Buytaert
committed
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
return;
}
context = context || document.body;
var $overlay, $tabbable, $hasTabindex;
// Determine which elements on the page already have a tabindex.
$hasTabindex = $('[tabindex] :not(.overlay-element)', context);
// Record the tabindex for each element, so we can restore it later.
$hasTabindex.each(Drupal.overlay._recordTabindex);
// Add the tabbable elements from the current context to any that we might
// have previously recorded.
Drupal.overlay._hasTabindex = $hasTabindex.add(Drupal.overlay._hasTabindex);
// Set tabindex to -1 on everything outside the overlay and toolbars, so that
// the underlying page is unreachable.
// By default, browsers make a, area, button, input, object, select, textarea,
// and iframe elements reachable via the tab key.
$tabbable = $('a, area, button, input, object, select, textarea, iframe');
// If another element (like a div) has a tabindex, it's also tabbable.
$tabbable = $tabbable.add($hasTabindex);
// Leave links inside the overlay and toolbars alone.
$overlay = $('.overlay-element, #overlay-container, .overlay-displace-top, .overlay-displace-bottom').find('*');
$tabbable = $tabbable.not($overlay);
// We now have a list of everything in the underlying document that could
// possibly be reachable via the tab key. Make it all unreachable.
$tabbable.attr('tabindex', -1);
};
/**
* Restores the original tabindex value of a group of elements.
*
* @param context
* The part of the DOM that should have its tabindexes restored. Defaults to
* the entire page.
*/
Drupal.overlay.makeDocumentTabbable = function (context) {
// Manipulating tabindexes is unacceptably slow in IE6 and IE7. In those
// browsers, the underlying page was never made unreachable via tab, so
// there is no work to be done here.
if (jQuery.browser.msie && parseInt(jQuery.browser.version, 10) < 8) {
return;
}
var $needsTabindex;
context = context || document.body;
// Make the underlying document tabbable again by removing all existing
// tabindex attributes.
var $tabindex = $('[tabindex]', context);
if (jQuery.browser.msie && parseInt(jQuery.browser.version, 10) < 8) {
// removeAttr('tabindex') is broken in IE6-7, but the DOM function
// removeAttribute works.
var i;
var length = $tabindex.length;
for (i = 0; i < length; i++) {
$tabindex[i].removeAttribute('tabIndex');
}
}
else {
$tabindex.removeAttr('tabindex');
}
// Restore the tabindex attributes that existed before the overlay was opened.
$needsTabindex = $(Drupal.overlay._hasTabindex, context);
$needsTabindex.each(Drupal.overlay._restoreTabindex);
Drupal.overlay._hasTabindex = Drupal.overlay._hasTabindex.not($needsTabindex);
};
/**
* Record the tabindex for an element, using $.data.
*
* Meant to be used as a jQuery.fn.each callback.
*/
Drupal.overlay._recordTabindex = function () {
var $element = $(this);
var tabindex = $(this).attr('tabindex');
$element.data('drupalOverlayOriginalTabIndex', tabindex);
Dries Buytaert
committed
};
Dries Buytaert
committed
/**
* Restore an element's original tabindex.
*
* Meant to be used as a jQuery.fn.each callback.
*/
Drupal.overlay._restoreTabindex = function () {
var $element = $(this);
var tabindex = $element.data('drupalOverlayOriginalTabIndex');
$element.attr('tabindex', tabindex);
};