Projet

Général

Profil

Paste
Télécharger (37,5 ko) Statistiques
| Branche: | Révision:

root / drupal7 / modules / overlay / overlay-parent.js @ a6e869e4

1
/**
2
 * @file
3
 * Attaches the behaviors for the Overlay parent pages.
4
 */
5

    
6
(function ($) {
7

    
8
/**
9
 * Open the overlay, or load content into it, when an admin link is clicked.
10
 */
11
Drupal.behaviors.overlayParent = {
12
  attach: function (context, settings) {
13
    if (Drupal.overlay.isOpen) {
14
      Drupal.overlay.makeDocumentUntabbable(context);
15
    }
16

    
17
    if (this.processed) {
18
      return;
19
    }
20
    this.processed = true;
21

    
22
    $(window)
23
      // When the hash (URL fragment) changes, open the overlay if needed.
24
      .bind('hashchange.drupal-overlay', $.proxy(Drupal.overlay, 'eventhandlerOperateByURLFragment'))
25
      // Trigger the hashchange handler once, after the page is loaded, so that
26
      // permalinks open the overlay.
27
      .triggerHandler('hashchange.drupal-overlay');
28

    
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
/**
39
 * Overlay object for parent windows.
40
 *
41
 * Events
42
 * Overlay triggers a number of events that can be used by other scripts.
43
 * - drupalOverlayOpen: This event is triggered when the overlay is opened.
44
 * - drupalOverlayBeforeClose: This event is triggered when the overlay attempts
45
 *   to close. If an event handler returns false, the close will be prevented.
46
 * - drupalOverlayClose: This event is triggered when the overlay is closed.
47
 * - drupalOverlayBeforeLoad: This event is triggered right before a new URL
48
 *   is loaded into the overlay.
49
 * - drupalOverlayReady: This event is triggered when the DOM of the overlay
50
 *   child document is fully loaded.
51
 * - drupalOverlayLoad: This event is triggered when the overlay is finished
52
 *   loading.
53
 * - drupalOverlayResize: This event is triggered when the overlay is being
54
 *   resized to match the parent window.
55
 */
56
Drupal.overlay = Drupal.overlay || {
57
  isOpen: false,
58
  isOpening: false,
59
  isClosing: false,
60
  isLoading: false
61
};
62

    
63
Drupal.overlay.prototype = {};
64

    
65
/**
66
 * Open the overlay.
67
 *
68
 * @param url
69
 *   The URL of the page to open in the overlay.
70
 *
71
 * @return
72
 *   TRUE if the overlay was opened, FALSE otherwise.
73
 */
74
Drupal.overlay.open = function (url) {
75
  // Just one overlay is allowed.
76
  if (this.isOpen || this.isOpening) {
77
    return this.load(url);
78
  }
79
  this.isOpening = true;
80
  // Store the original document title.
81
  this.originalTitle = document.title;
82

    
83
  // Create the dialog and related DOM elements.
84
  this.create();
85

    
86
  this.isOpening = false;
87
  this.isOpen = true;
88
  $(document.documentElement).addClass('overlay-open');
89
  this.makeDocumentUntabbable();
90

    
91
  // Allow other scripts to respond to this event.
92
  $(document).trigger('drupalOverlayOpen');
93

    
94
  return this.load(url);
95
};
96

    
97
/**
98
 * Create the underlying markup and behaviors for the overlay.
99
 */
100
Drupal.overlay.create = function () {
101
  this.$container = $(Drupal.theme('overlayContainer'))
102
    .appendTo(document.body);
103

    
104
  // Overlay uses transparent iframes that cover the full parent window.
105
  // When the overlay is open the scrollbar of the parent window is hidden.
106
  // Because some browsers show a white iframe background for a short moment
107
  // while loading a page into an iframe, overlay uses two iframes. By loading
108
  // the page in a hidden (inactive) iframe the user doesn't see the white
109
  // background. When the page is loaded the active and inactive iframes
110
  // are switched.
111
  this.activeFrame = this.$iframeA = $(Drupal.theme('overlayElement'))
112
    .appendTo(this.$container);
113

    
114
  this.inactiveFrame = this.$iframeB = $(Drupal.theme('overlayElement'))
115
    .appendTo(this.$container);
116

    
117
  this.$iframeA.bind('load.drupal-overlay', { self: this.$iframeA[0], sibling: this.$iframeB }, $.proxy(this, 'loadChild'));
118
  this.$iframeB.bind('load.drupal-overlay', { self: this.$iframeB[0], sibling: this.$iframeA }, $.proxy(this, 'loadChild'));
119

    
120
  // Add a second class "drupal-overlay-open" to indicate these event handlers
121
  // should only be bound when the overlay is open.
122
  var eventClass = '.drupal-overlay.drupal-overlay-open';
123
  $(window)
124
    .bind('resize' + eventClass, $.proxy(this, 'eventhandlerOuterResize'));
125
  $(document)
126
    .bind('drupalOverlayLoad' + eventClass, $.proxy(this, 'eventhandlerOuterResize'))
127
    .bind('drupalOverlayReady' + eventClass +
128
          ' drupalOverlayClose' + eventClass, $.proxy(this, 'eventhandlerSyncURLFragment'))
129
    .bind('drupalOverlayClose' + eventClass, $.proxy(this, 'eventhandlerRefreshPage'))
130
    .bind('drupalOverlayBeforeClose' + eventClass +
131
          ' drupalOverlayBeforeLoad' + eventClass +
132
          ' drupalOverlayResize' + eventClass, $.proxy(this, 'eventhandlerDispatchEvent'));
133

    
134
  if ($('.overlay-displace-top, .overlay-displace-bottom').length) {
135
    $(document)
136
      .bind('drupalOverlayResize' + eventClass, $.proxy(this, 'eventhandlerAlterDisplacedElements'))
137
      .bind('drupalOverlayClose' + eventClass, $.proxy(this, 'eventhandlerRestoreDisplacedElements'));
138
  }
139
};
140

    
141
/**
142
 * Load the given URL into the overlay iframe.
143
 *
144
 * Use this method to change the URL being loaded in the overlay if it is
145
 * already open.
146
 *
147
 * @return
148
 *   TRUE if URL is loaded into the overlay, FALSE otherwise.
149
 */
150
Drupal.overlay.load = function (url) {
151
  if (!this.isOpen) {
152
    return false;
153
  }
154

    
155
  // Allow other scripts to respond to this event.
156
  $(document).trigger('drupalOverlayBeforeLoad');
157

    
158
  $(document.documentElement).addClass('overlay-loading');
159

    
160
  // The contentDocument property is not supported in IE until IE8.
161
  var iframeDocument = this.inactiveFrame[0].contentDocument || this.inactiveFrame[0].contentWindow.document;
162

    
163
  // location.replace doesn't create a history entry. location.href does.
164
  // In this case, we want location.replace, as we're creating the history
165
  // entry using URL fragments.
166
  iframeDocument.location.replace(url);
167

    
168
  return true;
169
};
170

    
171
/**
172
 * Close the overlay and remove markup related to it from the document.
173
 *
174
 * @return
175
 *   TRUE if the overlay was closed, FALSE otherwise.
176
 */
177
Drupal.overlay.close = function () {
178
  // Prevent double execution when close is requested more than once.
179
  if (!this.isOpen || this.isClosing) {
180
    return false;
181
  }
182

    
183
  // Allow other scripts to respond to this event.
184
  var event = $.Event('drupalOverlayBeforeClose');
185
  $(document).trigger(event);
186
  // If a handler returned false, the close will be prevented.
187
  if (event.isDefaultPrevented()) {
188
    return false;
189
  }
190

    
191
  this.isClosing = true;
192
  this.isOpen = false;
193
  $(document.documentElement).removeClass('overlay-open');
194
  // Restore the original document title.
195
  document.title = this.originalTitle;
196
  this.makeDocumentTabbable();
197

    
198
  // Allow other scripts to respond to this event.
199
  $(document).trigger('drupalOverlayClose');
200

    
201
  // When the iframe is still loading don't destroy it immediately but after
202
  // the content is loaded (see Drupal.overlay.loadChild).
203
  if (!this.isLoading) {
204
    this.destroy();
205
    this.isClosing = false;
206
  }
207
  return true;
208
};
209

    
210
/**
211
 * Destroy the overlay.
212
 */
213
Drupal.overlay.destroy = function () {
214
  $([document, window]).unbind('.drupal-overlay-open');
215
  this.$container.remove();
216

    
217
  this.$container = null;
218
  this.$iframeA = null;
219
  this.$iframeB = null;
220

    
221
  this.iframeWindow = null;
222
};
223

    
224
/**
225
 * Redirect the overlay parent window to the given URL.
226
 *
227
 * @param url
228
 *   Can be an absolute URL or a relative link to the domain root.
229
 */
230
Drupal.overlay.redirect = function (url) {
231
  // Create a native Link object, so we can use its object methods.
232
  var link = $(url.link(url)).get(0);
233

    
234
  // If the link is already open, force the hashchange event to simulate reload.
235
  if (window.location.href == link.href) {
236
    $(window).triggerHandler('hashchange.drupal-overlay');
237
  }
238

    
239
  window.location.href = link.href;
240
  return true;
241
};
242

    
243
/**
244
 * Bind the child window.
245
 *
246
 * Note that this function is fired earlier than Drupal.overlay.loadChild.
247
 */
248
Drupal.overlay.bindChild = function (iframeWindow, isClosing) {
249
  this.iframeWindow = iframeWindow;
250

    
251
  // We are done if the child window is closing.
252
  if (isClosing || this.isClosing || !this.isOpen) {
253
    return;
254
  }
255

    
256
  // Allow other scripts to respond to this event.
257
  $(document).trigger('drupalOverlayReady');
258
};
259

    
260
/**
261
 * Event handler: load event handler for the overlay iframe.
262
 *
263
 * @param event
264
 *   Event being triggered, with the following restrictions:
265
 *   - event.type: load
266
 *   - event.currentTarget: iframe
267
 */
268
Drupal.overlay.loadChild = function (event) {
269
  var iframe = event.data.self;
270
  var iframeDocument = iframe.contentDocument || iframe.contentWindow.document;
271
  var iframeWindow = iframeDocument.defaultView || iframeDocument.parentWindow;
272
  if (iframeWindow.location == 'about:blank') {
273
    return;
274
  }
275

    
276
  this.isLoading = false;
277
  $(document.documentElement).removeClass('overlay-loading');
278
  event.data.sibling.removeClass('overlay-active').attr({ 'tabindex': -1 });
279

    
280
  // Only continue when overlay is still open and not closing.
281
  if (this.isOpen && !this.isClosing) {
282
    // And child document is an actual overlayChild.
283
    if (iframeWindow.Drupal && iframeWindow.Drupal.overlayChild) {
284
      // Replace the document title with title of iframe.
285
      document.title = iframeWindow.document.title;
286

    
287
      this.activeFrame = $(iframe)
288
        .addClass('overlay-active')
289
        // Add a title attribute to the iframe for accessibility.
290
        .attr('title', Drupal.t('@title dialog', { '@title': iframeWindow.jQuery('#overlay-title').text() })).removeAttr('tabindex');
291
      this.inactiveFrame = event.data.sibling;
292

    
293
      // Load an empty document into the inactive iframe.
294
      (this.inactiveFrame[0].contentDocument || this.inactiveFrame[0].contentWindow.document).location.replace('about:blank');
295

    
296
      // Move the focus to just before the "skip to main content" link inside
297
      // the overlay.
298
      this.activeFrame.focus();
299
      var skipLink = iframeWindow.jQuery('a:first');
300
      Drupal.overlay.setFocusBefore(skipLink, iframeWindow.document);
301

    
302
      // Allow other scripts to respond to this event.
303
      $(document).trigger('drupalOverlayLoad');
304
    }
305
    else {
306
      window.location = iframeWindow.location.href.replace(/([?&]?)render=overlay&?/g, '$1').replace(/\?$/, '');
307
    }
308
  }
309
  else {
310
    this.destroy();
311
  }
312
};
313

    
314
/**
315
 * Creates a placeholder element to receive document focus.
316
 *
317
 * Setting the document focus to a link will make it visible, even if it's a
318
 * "skip to main content" link that should normally be visible only when the
319
 * user tabs to it. This function can be used to set the document focus to
320
 * just before such an invisible link.
321
 *
322
 * @param $element
323
 *   The jQuery element that should receive focus on the next tab press.
324
 * @param document
325
 *   The iframe window element to which the placeholder should be added. The
326
 *   placeholder element has to be created inside the same iframe as the element
327
 *   it precedes, to keep IE happy. (http://bugs.jquery.com/ticket/4059)
328
 */
329
Drupal.overlay.setFocusBefore = function ($element, document) {
330
  // Create an anchor inside the placeholder document.
331
  var placeholder = document.createElement('a');
332
  var $placeholder = $(placeholder).addClass('element-invisible').attr('href', '#');
333
  // Put the placeholder where it belongs, and set the document focus to it.
334
  $placeholder.insertBefore($element);
335
  $placeholder.focus();
336
  // Make the placeholder disappear as soon as it loses focus, so that it
337
  // doesn't appear in the tab order again.
338
  $placeholder.one('blur', function () {
339
    $(this).remove();
340
  });
341
};
342

    
343
/**
344
 * Check if the given link is in the administrative section of the site.
345
 *
346
 * @param url
347
 *   The URL to be tested.
348
 *
349
 * @return boolean
350
 *   TRUE if the URL represents an administrative link, FALSE otherwise.
351
 */
352
Drupal.overlay.isAdminLink = function (url) {
353
  if (Drupal.overlay.isExternalLink(url)) {
354
    return false;
355
  }
356

    
357
  var path = this.getPath(url);
358

    
359
  // Turn the list of administrative paths into a regular expression.
360
  if (!this.adminPathRegExp) {
361
    var prefix = '';
362
    if (Drupal.settings.overlay.pathPrefixes.length) {
363
      // Allow path prefixes used for language negatiation followed by slash,
364
      // and the empty string.
365
      prefix = '(' + Drupal.settings.overlay.pathPrefixes.join('/|') + '/|)';
366
    }
367
    var adminPaths = '^' + prefix + '(' + Drupal.settings.overlay.paths.admin.replace(/\s+/g, '|') + ')$';
368
    var nonAdminPaths = '^' + prefix + '(' + Drupal.settings.overlay.paths.non_admin.replace(/\s+/g, '|') + ')$';
369
    adminPaths = adminPaths.replace(/\*/g, '.*');
370
    nonAdminPaths = nonAdminPaths.replace(/\*/g, '.*');
371
    this.adminPathRegExp = new RegExp(adminPaths);
372
    this.nonAdminPathRegExp = new RegExp(nonAdminPaths);
373
  }
374

    
375
  return this.adminPathRegExp.exec(path) && !this.nonAdminPathRegExp.exec(path);
376
};
377

    
378
/**
379
 * Determine whether a link is external to the site.
380
 *
381
 * @param url
382
 *   The URL to be tested.
383
 *
384
 * @return boolean
385
 *   TRUE if the URL is external to the site, FALSE otherwise.
386
 */
387
Drupal.overlay.isExternalLink = function (url) {
388
  var re = RegExp('^((f|ht)tps?:)?//(?!' + window.location.host + ')');
389
  return re.test(url);
390
};
391

    
392
/**
393
 * Constructs an internal URL (relative to this site) from the provided path.
394
 *
395
 * For example, if the provided path is 'admin' and the site is installed at
396
 * http://example.com/drupal, this function will return '/drupal/admin'.
397
 *
398
 * @param path
399
 *   The internal path, without any leading slash.
400
 *
401
 * @return
402
 *   The internal URL derived from the provided path, or null if a valid
403
 *   internal path cannot be constructed (for example, if an attempt to create
404
 *   an external link is detected).
405
 */
406
Drupal.overlay.getInternalUrl = function (path) {
407
  var url = Drupal.settings.basePath + path;
408
  if (!this.isExternalLink(url)) {
409
    return url;
410
  }
411
};
412

    
413
/**
414
 * Event handler: resizes overlay according to the size of the parent window.
415
 *
416
 * @param event
417
 *   Event being triggered, with the following restrictions:
418
 *   - event.type: any
419
 *   - event.currentTarget: any
420
 */
421
Drupal.overlay.eventhandlerOuterResize = function (event) {
422
  // Proceed only if the overlay still exists.
423
  if (!(this.isOpen || this.isOpening) || this.isClosing || !this.iframeWindow) {
424
    return;
425
  }
426

    
427
  // IE6 uses position:absolute instead of position:fixed.
428
  if (typeof document.body.style.maxHeight != 'string') {
429
    this.activeFrame.height($(window).height());
430
  }
431

    
432
  // Allow other scripts to respond to this event.
433
  $(document).trigger('drupalOverlayResize');
434
};
435

    
436
/**
437
 * Event handler: resizes displaced elements so they won't overlap the scrollbar
438
 * of overlay's iframe.
439
 *
440
 * @param event
441
 *   Event being triggered, with the following restrictions:
442
 *   - event.type: any
443
 *   - event.currentTarget: any
444
 */
445
Drupal.overlay.eventhandlerAlterDisplacedElements = function (event) {
446
  // Proceed only if the overlay still exists.
447
  if (!(this.isOpen || this.isOpening) || this.isClosing || !this.iframeWindow) {
448
    return;
449
  }
450

    
451
  $(this.iframeWindow.document.body).css({
452
    marginTop: Drupal.overlay.getDisplacement('top'),
453
    marginBottom: Drupal.overlay.getDisplacement('bottom')
454
  })
455
  // IE7 isn't reflowing the document immediately.
456
  // @todo This might be fixed in a cleaner way.
457
  .addClass('overlay-trigger-reflow').removeClass('overlay-trigger-reflow');
458

    
459
  var documentHeight = this.iframeWindow.document.body.clientHeight;
460
  var documentWidth = this.iframeWindow.document.body.clientWidth;
461
  // IE6 doesn't support maxWidth, use width instead.
462
  var maxWidthName = (typeof document.body.style.maxWidth == 'string') ? 'maxWidth' : 'width';
463

    
464
  if (Drupal.overlay.leftSidedScrollbarOffset === undefined && $(document.documentElement).attr('dir') === 'rtl') {
465
    // We can't use element.clientLeft to detect whether scrollbars are placed
466
    // on the left side of the element when direction is set to "rtl" as most
467
    // browsers dont't support it correctly.
468
    // http://www.gtalbot.org/BugzillaSection/DocumentAllDHTMLproperties.html
469
    // There seems to be absolutely no way to detect whether the scrollbar
470
    // is on the left side in Opera; always expect scrollbar to be on the left.
471
    if ($.browser.opera) {
472
      Drupal.overlay.leftSidedScrollbarOffset = document.documentElement.clientWidth - this.iframeWindow.document.documentElement.clientWidth + this.iframeWindow.document.documentElement.clientLeft;
473
    }
474
    else if (this.iframeWindow.document.documentElement.clientLeft) {
475
      Drupal.overlay.leftSidedScrollbarOffset = this.iframeWindow.document.documentElement.clientLeft;
476
    }
477
    else {
478
      var el1 = $('<div style="direction: rtl; overflow: scroll;"></div>').appendTo(document.body);
479
      var el2 = $('<div></div>').appendTo(el1);
480
      Drupal.overlay.leftSidedScrollbarOffset = parseInt(el2[0].offsetLeft - el1[0].offsetLeft);
481
      el1.remove();
482
    }
483
  }
484

    
485
  // Consider any element that should be visible above the overlay (such as
486
  // a toolbar).
487
  $('.overlay-displace-top, .overlay-displace-bottom').each(function () {
488
    var data = $(this).data();
489
    var maxWidth = documentWidth;
490
    // In IE, Shadow filter makes element to overlap the scrollbar with 1px.
491
    if (this.filters && this.filters.length && this.filters.item('DXImageTransform.Microsoft.Shadow')) {
492
      maxWidth -= 1;
493
    }
494

    
495
    if (Drupal.overlay.leftSidedScrollbarOffset) {
496
      $(this).css('left', Drupal.overlay.leftSidedScrollbarOffset);
497
    }
498

    
499
    // Prevent displaced elements overlapping window's scrollbar.
500
    var currentMaxWidth = parseInt($(this).css(maxWidthName));
501
    if ((data.drupalOverlay && data.drupalOverlay.maxWidth) || isNaN(currentMaxWidth) || currentMaxWidth > maxWidth || currentMaxWidth <= 0) {
502
      $(this).css(maxWidthName, maxWidth);
503
      (data.drupalOverlay = data.drupalOverlay || {}).maxWidth = true;
504
    }
505

    
506
    // Use a more rigorous approach if the displaced element still overlaps
507
    // window's scrollbar; clip the element on the right.
508
    var offset = $(this).offset();
509
    var offsetRight = offset.left + $(this).outerWidth();
510
    if ((data.drupalOverlay && data.drupalOverlay.clip) || offsetRight > maxWidth) {
511
      if (Drupal.overlay.leftSidedScrollbarOffset) {
512
        $(this).css('clip', 'rect(auto, auto, ' + (documentHeight - offset.top) + 'px, ' + (Drupal.overlay.leftSidedScrollbarOffset + 2) + 'px)');
513
      }
514
      else {
515
        $(this).css('clip', 'rect(auto, ' + (maxWidth - offset.left) + 'px, ' + (documentHeight - offset.top) + 'px, auto)');
516
      }
517
      (data.drupalOverlay = data.drupalOverlay || {}).clip = true;
518
    }
519
  });
520
};
521

    
522
/**
523
 * Event handler: restores size of displaced elements as they were before
524
 * overlay was opened.
525
 *
526
 * @param event
527
 *   Event being triggered, with the following restrictions:
528
 *   - event.type: any
529
 *   - event.currentTarget: any
530
 */
531
Drupal.overlay.eventhandlerRestoreDisplacedElements = function (event) {
532
  var $displacedElements = $('.overlay-displace-top, .overlay-displace-bottom');
533
  try {
534
    $displacedElements.css({ maxWidth: '', clip: '' });
535
  }
536
  // IE bug that doesn't allow unsetting style.clip (http://dev.jquery.com/ticket/6512).
537
  catch (err) {
538
    $displacedElements.attr('style', function (index, attr) {
539
      return attr.replace(/clip\s*:\s*rect\([^)]+\);?/i, '');
540
    });
541
  }
542
};
543

    
544
/**
545
 * Event handler: overrides href of administrative links to be opened in
546
 * the overlay.
547
 *
548
 * This click event handler should be bound to any document (for example the
549
 * overlay iframe) of which you want links to open in the overlay.
550
 *
551
 * @param event
552
 *   Event being triggered, with the following restrictions:
553
 *   - event.type: click, mouseup
554
 *   - event.currentTarget: document
555
 *
556
 * @see Drupal.overlayChild.behaviors.addClickHandler
557
 */
558
Drupal.overlay.eventhandlerOverrideLink = function (event) {
559
  // In some browsers the click event isn't fired for right-clicks. Use the
560
  // mouseup event for right-clicks and the click event for everything else.
561
  if ((event.type == 'click' && event.button == 2) || (event.type == 'mouseup' && event.button != 2)) {
562
    return;
563
  }
564

    
565
  var $target = $(event.target);
566

    
567
  // Only continue if clicked target (or one of its parents) is a link.
568
  if (!$target.is('a')) {
569
    $target = $target.closest('a');
570
    if (!$target.length) {
571
      return;
572
    }
573
  }
574

    
575
  // Never open links in the overlay that contain the overlay-exclude class.
576
  if ($target.hasClass('overlay-exclude')) {
577
    return;
578
  }
579

    
580
  // Close the overlay when the link contains the overlay-close class.
581
  if ($target.hasClass('overlay-close')) {
582
    // Clearing the overlay URL fragment will close the overlay.
583
    $.bbq.removeState('overlay');
584
    return;
585
  }
586

    
587
  var target = $target[0];
588
  var href = target.href;
589
  // Only handle links that have an href attribute and use the HTTP(S) protocol.
590
  if (href != undefined && href != '' && target.protocol.match(/^https?\:/)) {
591
    var anchor = href.replace(target.ownerDocument.location.href, '');
592
    // Skip anchor links.
593
    if (anchor.length == 0 || anchor.charAt(0) == '#') {
594
      return;
595
    }
596
    // Open admin links in the overlay.
597
    else if (this.isAdminLink(href)) {
598
      // If the link contains the overlay-restore class and the overlay-context
599
      // state is set, also update the parent window's location.
600
      var parentLocation = ($target.hasClass('overlay-restore') && typeof $.bbq.getState('overlay-context') == 'string')
601
        ? this.getInternalUrl($.bbq.getState('overlay-context'))
602
        : null;
603
      href = this.fragmentizeLink($target.get(0), parentLocation);
604
      // Only override default behavior when left-clicking and user is not
605
      // pressing the ALT, CTRL, META (Command key on the Macintosh keyboard)
606
      // or SHIFT key.
607
      if (event.button == 0 && !event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey) {
608
        // Redirect to a fragmentized href. This will trigger a hashchange event.
609
        this.redirect(href);
610
        // Prevent default action and further propagation of the event.
611
        return false;
612
      }
613
      // Otherwise alter clicked link's href. This is being picked up by
614
      // the default action handler.
615
      else {
616
        $target
617
          // Restore link's href attribute on blur or next click.
618
          .one('blur mousedown', { target: target, href: target.href }, function (event) { $(event.data.target).attr('href', event.data.href); })
619
          .attr('href', href);
620
      }
621
    }
622
    // Non-admin links should close the overlay and open in the main window,
623
    // which is the default action for a link. We only need to handle them
624
    // if the overlay is open and the clicked link is inside the overlay iframe.
625
    else if (this.isOpen && target.ownerDocument === this.iframeWindow.document) {
626
      // Open external links in the immediate parent of the frame, unless the
627
      // link already has a different target.
628
      if (target.hostname != window.location.hostname) {
629
        if (!$target.attr('target')) {
630
          $target.attr('target', '_parent');
631
        }
632
      }
633
      else {
634
        // Add the overlay-context state to the link, so "overlay-restore" links
635
        // can restore the context.
636
        if ($target[0].hash) {
637
          // Leave links with an existing fragment alone. Adding an extra
638
          // parameter to a link like "node/1#section-1" breaks the link.
639
        }
640
        else {
641
          // For links with no existing fragment, add the overlay context.
642
          $target.attr('href', $.param.fragment(href, { 'overlay-context': this.getPath(window.location) + window.location.search }));
643
        }
644

    
645
        // When the link has a destination query parameter and that destination
646
        // is an admin link we need to fragmentize it. This will make it reopen
647
        // in the overlay.
648
        var params = $.deparam.querystring(href);
649
        if (params.destination && this.isAdminLink(params.destination)) {
650
          var fragmentizedDestination = $.param.fragment(this.getPath(window.location), { overlay: params.destination });
651
          $target.attr('href', $.param.querystring(href, { destination: fragmentizedDestination }));
652
        }
653

    
654
        // Make the link open in the immediate parent of the frame, unless the
655
        // link already has a different target.
656
        if (!$target.attr('target')) {
657
          $target.attr('target', '_parent');
658
        }
659
      }
660
    }
661
  }
662
};
663

    
664
/**
665
 * Event handler: opens or closes the overlay based on the current URL fragment.
666
 *
667
 * @param event
668
 *   Event being triggered, with the following restrictions:
669
 *   - event.type: hashchange
670
 *   - event.currentTarget: document
671
 */
672
Drupal.overlay.eventhandlerOperateByURLFragment = function (event) {
673
  // If we changed the hash to reflect an internal redirect in the overlay,
674
  // its location has already been changed, so don't do anything.
675
  if ($.data(window.location, window.location.href) === 'redirect') {
676
    $.data(window.location, window.location.href, null);
677
    return;
678
  }
679

    
680
  // Get the overlay URL from the current URL fragment.
681
  var internalUrl = null;
682
  var state = $.bbq.getState('overlay');
683
  if (state) {
684
    internalUrl = this.getInternalUrl(state);
685
  }
686
  if (internalUrl) {
687
    // Append render variable, so the server side can choose the right
688
    // rendering and add child frame code to the page if needed.
689
    var url = $.param.querystring(internalUrl, { render: 'overlay' });
690

    
691
    this.open(url);
692
    this.resetActiveClass(this.getPath(Drupal.settings.basePath + state));
693
  }
694
  // If there is no overlay URL in the fragment and the overlay is (still)
695
  // open, close the overlay.
696
  else if (this.isOpen && !this.isClosing) {
697
    this.close();
698
    this.resetActiveClass(this.getPath(window.location));
699
  }
700
};
701

    
702
/**
703
 * Event handler: makes sure the internal overlay URL is reflected in the parent
704
 * URL fragment.
705
 *
706
 * Normally the parent URL fragment determines the overlay location. However, if
707
 * the overlay redirects internally, the parent doesn't get informed, and the
708
 * parent URL fragment will be out of date. This is a sanity check to make
709
 * sure we're in the right place.
710
 *
711
 * The parent URL fragment is also not updated automatically when overlay's
712
 * open, close or load functions are used directly (instead of through
713
 * eventhandlerOperateByURLFragment).
714
 *
715
 * @param event
716
 *   Event being triggered, with the following restrictions:
717
 *   - event.type: drupalOverlayReady, drupalOverlayClose
718
 *   - event.currentTarget: document
719
 */
720
Drupal.overlay.eventhandlerSyncURLFragment = function (event) {
721
  if (this.isOpen) {
722
    var expected = $.bbq.getState('overlay');
723
    // This is just a sanity check, so we're comparing paths, not query strings.
724
    if (this.getPath(Drupal.settings.basePath + expected) != this.getPath(this.iframeWindow.document.location)) {
725
      // There may have been a redirect inside the child overlay window that the
726
      // parent wasn't aware of. Update the parent URL fragment appropriately.
727
      var newLocation = Drupal.overlay.fragmentizeLink(this.iframeWindow.document.location);
728
      // Set a 'redirect' flag on the new location so the hashchange event handler
729
      // knows not to change the overlay's content.
730
      $.data(window.location, newLocation, 'redirect');
731
      // Use location.replace() so we don't create an extra history entry.
732
      window.location.replace(newLocation);
733
    }
734
  }
735
  else {
736
    $.bbq.removeState('overlay');
737
  }
738
};
739

    
740
/**
741
 * Event handler: if the child window suggested that the parent refresh on
742
 * close, force a page refresh.
743
 *
744
 * @param event
745
 *   Event being triggered, with the following restrictions:
746
 *   - event.type: drupalOverlayClose
747
 *   - event.currentTarget: document
748
 */
749
Drupal.overlay.eventhandlerRefreshPage = function (event) {
750
  if (Drupal.overlay.refreshPage) {
751
    window.location.reload(true);
752
  }
753
};
754

    
755
/**
756
 * Event handler: dispatches events to the overlay document.
757
 *
758
 * @param event
759
 *   Event being triggered, with the following restrictions:
760
 *   - event.type: any
761
 *   - event.currentTarget: any
762
 */
763
Drupal.overlay.eventhandlerDispatchEvent = function (event) {
764
  if (this.iframeWindow && this.iframeWindow.document) {
765
    this.iframeWindow.jQuery(this.iframeWindow.document).trigger(event);
766
  }
767
};
768

    
769
/**
770
 * Make a regular admin link into a URL that will trigger the overlay to open.
771
 *
772
 * @param link
773
 *   A JavaScript Link object (i.e. an <a> element).
774
 * @param parentLocation
775
 *   (optional) URL to override the parent window's location with.
776
 *
777
 * @return
778
 *   A URL that will trigger the overlay (in the form
779
 *   /node/1#overlay=admin/config).
780
 */
781
Drupal.overlay.fragmentizeLink = function (link, parentLocation) {
782
  // Don't operate on links that are already overlay-ready.
783
  var params = $.deparam.fragment(link.href);
784
  if (params.overlay) {
785
    return link.href;
786
  }
787

    
788
  // Determine the link's original destination. Set ignorePathFromQueryString to
789
  // true to prevent transforming this link into a clean URL while clean URLs
790
  // may be disabled.
791
  var path = this.getPath(link, true);
792
  // Preserve existing query and fragment parameters in the URL, except for
793
  // "render=overlay" which is re-added in Drupal.overlay.eventhandlerOperateByURLFragment.
794
  var destination = path + link.search.replace(/&?render=overlay/, '').replace(/\?$/, '') + link.hash;
795

    
796
  // Assemble and return the overlay-ready link.
797
  return $.param.fragment(parentLocation || window.location.href, { overlay: destination });
798
};
799

    
800
/**
801
 * Refresh any regions of the page that are displayed outside the overlay.
802
 *
803
 * @param data
804
 *   An array of objects with information on the page regions to be refreshed.
805
 *   For each object, the key is a CSS class identifying the region to be
806
 *   refreshed, and the value represents the section of the Drupal $page array
807
 *   corresponding to this region.
808
 */
809
Drupal.overlay.refreshRegions = function (data) {
810
  $.each(data, function () {
811
    var region_info = this;
812
    $.each(region_info, function (regionClass) {
813
      var regionName = region_info[regionClass];
814
      var regionSelector = '.' + regionClass;
815
      // Allow special behaviors to detach.
816
      Drupal.detachBehaviors($(regionSelector));
817
      $.get(Drupal.settings.basePath + Drupal.settings.overlay.ajaxCallback + '/' + regionName, function (newElement) {
818
        $(regionSelector).replaceWith($(newElement));
819
        Drupal.attachBehaviors($(regionSelector), Drupal.settings);
820
      });
821
    });
822
  });
823
};
824

    
825
/**
826
 * Reset the active class on links in displaced elements according to
827
 * given path.
828
 *
829
 * @param activePath
830
 *   Path to match links against.
831
 */
832
Drupal.overlay.resetActiveClass = function(activePath) {
833
  var self = this;
834
  var windowDomain = window.location.protocol + window.location.hostname;
835

    
836
  $('.overlay-displace-top, .overlay-displace-bottom')
837
  .find('a[href]')
838
  // Remove active class from all links in displaced elements.
839
  .removeClass('active')
840
  // Add active class to links that match activePath.
841
  .each(function () {
842
    var linkDomain = this.protocol + this.hostname;
843
    var linkPath = self.getPath(this);
844

    
845
    // A link matches if it is part of the active trail of activePath, except
846
    // for frontpage links.
847
    if (linkDomain == windowDomain && (activePath + '/').indexOf(linkPath + '/') === 0 && (linkPath !== '' || activePath === '')) {
848
      $(this).addClass('active');
849
    }
850
  });
851
};
852

    
853
/**
854
 * Helper function to get the (corrected) Drupal path of a link.
855
 *
856
 * @param link
857
 *   Link object or string to get the Drupal path from.
858
 * @param ignorePathFromQueryString
859
 *   Boolean whether to ignore path from query string if path appears empty.
860
 *
861
 * @return
862
 *   The Drupal path.
863
 */
864
Drupal.overlay.getPath = function (link, ignorePathFromQueryString) {
865
  if (typeof link == 'string') {
866
    // Create a native Link object, so we can use its object methods.
867
    link = $(link.link(link)).get(0);
868
  }
869

    
870
  var path = link.pathname;
871
  // Ensure a leading slash on the path, omitted in some browsers.
872
  if (path.charAt(0) != '/') {
873
    path = '/' + path;
874
  }
875
  path = path.replace(new RegExp(Drupal.settings.basePath + '(?:index.php)?'), '');
876
  if (path == '' && !ignorePathFromQueryString) {
877
    // If the path appears empty, it might mean the path is represented in the
878
    // query string (clean URLs are not used).
879
    var match = new RegExp('([?&])q=(.+)([&#]|$)').exec(link.search);
880
    if (match && match.length == 4) {
881
      path = match[2];
882
    }
883
  }
884

    
885
  return path;
886
};
887

    
888
/**
889
 * Get the total displacement of given region.
890
 *
891
 * @param region
892
 *   Region name. Either "top" or "bottom".
893
 *
894
 * @return
895
 *   The total displacement of given region in pixels.
896
 */
897
Drupal.overlay.getDisplacement = function (region) {
898
  var displacement = 0;
899
  var lastDisplaced = $('.overlay-displace-' + region + ':last');
900
  if (lastDisplaced.length) {
901
    displacement = lastDisplaced.offset().top + lastDisplaced.outerHeight();
902

    
903
    // In modern browsers (including IE9), when box-shadow is defined, use the
904
    // normal height.
905
    var cssBoxShadowValue = lastDisplaced.css('box-shadow');
906
    var boxShadow = (typeof cssBoxShadowValue !== 'undefined' && cssBoxShadowValue !== 'none');
907
    // In IE8 and below, we use the shadow filter to apply box-shadow styles to
908
    // the toolbar. It adds some extra height that we need to remove.
909
    if (!boxShadow && /DXImageTransform\.Microsoft\.Shadow/.test(lastDisplaced.css('filter'))) {
910
      displacement -= lastDisplaced[0].filters.item('DXImageTransform.Microsoft.Shadow').strength;
911
      displacement = Math.max(0, displacement);
912
    }
913
  }
914
  return displacement;
915
};
916

    
917
/**
918
 * Makes elements outside the overlay unreachable via the tab key.
919
 *
920
 * @param context
921
 *   The part of the DOM that should have its tabindexes changed. Defaults to
922
 *   the entire page.
923
 */
924
Drupal.overlay.makeDocumentUntabbable = function (context) {
925
  // Manipulating tabindexes for the entire document is unacceptably slow in IE6
926
  // and IE7, so in those browsers, the underlying page will still be reachable
927
  // via the tab key. However, we still make the links within the Disable
928
  // message unreachable, because the same message also exists within the
929
  // child document. The duplicate copy in the underlying document is only for
930
  // assisting screen-reader users navigating the document with reading commands
931
  // that follow markup order rather than tab order.
932
  if (jQuery.browser.msie && parseInt(jQuery.browser.version, 10) < 8) {
933
    $('#overlay-disable-message a', context).attr('tabindex', -1);
934
    return;
935
  }
936

    
937
  context = context || document.body;
938
  var $overlay, $tabbable, $hasTabindex;
939

    
940
  // Determine which elements on the page already have a tabindex.
941
  $hasTabindex = $('[tabindex] :not(.overlay-element)', context);
942
  // Record the tabindex for each element, so we can restore it later.
943
  $hasTabindex.each(Drupal.overlay._recordTabindex);
944
  // Add the tabbable elements from the current context to any that we might
945
  // have previously recorded.
946
  Drupal.overlay._hasTabindex = $hasTabindex.add(Drupal.overlay._hasTabindex);
947

    
948
  // Set tabindex to -1 on everything outside the overlay and toolbars, so that
949
  // the underlying page is unreachable.
950

    
951
  // By default, browsers make a, area, button, input, object, select, textarea,
952
  // and iframe elements reachable via the tab key.
953
  $tabbable = $('a, area, button, input, object, select, textarea, iframe');
954
  // If another element (like a div) has a tabindex, it's also tabbable.
955
  $tabbable = $tabbable.add($hasTabindex);
956
  // Leave links inside the overlay and toolbars alone.
957
  $overlay = $('.overlay-element, #overlay-container, .overlay-displace-top, .overlay-displace-bottom').find('*');
958
  $tabbable = $tabbable.not($overlay);
959
  // We now have a list of everything in the underlying document that could
960
  // possibly be reachable via the tab key. Make it all unreachable.
961
  $tabbable.attr('tabindex', -1);
962
};
963

    
964
/**
965
 * Restores the original tabindex value of a group of elements.
966
 *
967
 * @param context
968
 *   The part of the DOM that should have its tabindexes restored. Defaults to
969
 *   the entire page.
970
 */
971
Drupal.overlay.makeDocumentTabbable = function (context) {
972
  // Manipulating tabindexes is unacceptably slow in IE6 and IE7. In those
973
  // browsers, the underlying page was never made unreachable via tab, so
974
  // there is no work to be done here.
975
  if (jQuery.browser.msie && parseInt(jQuery.browser.version, 10) < 8) {
976
    return;
977
  }
978

    
979
  var $needsTabindex;
980
  context = context || document.body;
981

    
982
  // Make the underlying document tabbable again by removing all existing
983
  // tabindex attributes.
984
  var $tabindex = $('[tabindex]', context);
985
  if (jQuery.browser.msie && parseInt(jQuery.browser.version, 10) < 8) {
986
    // removeAttr('tabindex') is broken in IE6-7, but the DOM function
987
    // removeAttribute works.
988
    var i;
989
    var length = $tabindex.length;
990
    for (i = 0; i < length; i++) {
991
      $tabindex[i].removeAttribute('tabIndex');
992
    }
993
  }
994
  else {
995
    $tabindex.removeAttr('tabindex');
996
  }
997

    
998
  // Restore the tabindex attributes that existed before the overlay was opened.
999
  $needsTabindex = $(Drupal.overlay._hasTabindex, context);
1000
  $needsTabindex.each(Drupal.overlay._restoreTabindex);
1001
  Drupal.overlay._hasTabindex = Drupal.overlay._hasTabindex.not($needsTabindex);
1002
};
1003

    
1004
/**
1005
 * Record the tabindex for an element, using $.data.
1006
 *
1007
 * Meant to be used as a jQuery.fn.each callback.
1008
 */
1009
Drupal.overlay._recordTabindex = function () {
1010
  var $element = $(this);
1011
  var tabindex = $(this).attr('tabindex');
1012
  $element.data('drupalOverlayOriginalTabIndex', tabindex);
1013
};
1014

    
1015
/**
1016
 * Restore an element's original tabindex.
1017
 *
1018
 * Meant to be used as a jQuery.fn.each callback.
1019
 */
1020
Drupal.overlay._restoreTabindex = function () {
1021
  var $element = $(this);
1022
  var tabindex = $element.data('drupalOverlayOriginalTabIndex');
1023
  $element.attr('tabindex', tabindex);
1024
};
1025

    
1026
/**
1027
 * Theme function to create the overlay iframe element.
1028
 */
1029
Drupal.theme.prototype.overlayContainer = function () {
1030
  return '<div id="overlay-container"><div class="overlay-modal-background"></div></div>';
1031
};
1032

    
1033
/**
1034
 * Theme function to create an overlay iframe element.
1035
 */
1036
Drupal.theme.prototype.overlayElement = function (url) {
1037
  return '<iframe class="overlay-element" frameborder="0" scrolling="auto" allowtransparency="true"></iframe>';
1038
};
1039

    
1040
})(jQuery);