2015-08-17 17:00:26 -07:00
/ * *
* @ file
* Attaches behaviors for the Contextual module .
* /
( function ( $ , Drupal , drupalSettings , _ , Backbone , JSON , storage ) {
2015-10-21 21:44:50 -07:00
'use strict' ;
2015-08-17 17:00:26 -07:00
var options = $ . extend ( drupalSettings . contextual ,
// Merge strings on top of drupalSettings so that they are not mutable.
{
strings : {
open : Drupal . t ( 'Open' ) ,
close : Drupal . t ( 'Close' )
}
}
) ;
// Clear the cached contextual links whenever the current user's set of
// permissions changes.
var cachedPermissionsHash = storage . getItem ( 'Drupal.contextual.permissionsHash' ) ;
var permissionsHash = drupalSettings . user . permissionsHash ;
if ( cachedPermissionsHash !== permissionsHash ) {
if ( typeof permissionsHash === 'string' ) {
_ . chain ( storage ) . keys ( ) . each ( function ( key ) {
if ( key . substring ( 0 , 18 ) === 'Drupal.contextual.' ) {
storage . removeItem ( key ) ;
}
} ) ;
}
storage . setItem ( 'Drupal.contextual.permissionsHash' , permissionsHash ) ;
}
/ * *
* Initializes a contextual link : updates its DOM , sets up model and views .
*
* @ param { jQuery } $contextual
* A contextual links placeholder DOM element , containing the actual
* contextual links as rendered by the server .
* @ param { string } html
* The server - side rendered HTML for this contextual link .
* /
function initContextual ( $contextual , html ) {
var $region = $contextual . closest ( '.contextual-region' ) ;
var contextual = Drupal . contextual ;
$contextual
// Update the placeholder to contain its rendered contextual links.
. html ( html )
// Use the placeholder as a wrapper with a specific class to provide
// positioning and behavior attachment context.
. addClass ( 'contextual' )
// Ensure a trigger element exists before the actual contextual links.
. prepend ( Drupal . theme ( 'contextualTrigger' ) ) ;
// Set the destination parameter on each of the contextual links.
var destination = 'destination=' + Drupal . encodePath ( drupalSettings . path . currentPath ) ;
$contextual . find ( '.contextual-links a' ) . each ( function ( ) {
var url = this . getAttribute ( 'href' ) ;
var glue = ( url . indexOf ( '?' ) === - 1 ) ? '?' : '&' ;
this . setAttribute ( 'href' , url + glue + destination ) ;
} ) ;
// Create a model and the appropriate views.
var model = new contextual . StateModel ( {
title : $region . find ( 'h2' ) . eq ( 0 ) . text ( ) . trim ( )
} ) ;
var viewOptions = $ . extend ( { el : $contextual , model : model } , options ) ;
contextual . views . push ( {
visual : new contextual . VisualView ( viewOptions ) ,
aural : new contextual . AuralView ( viewOptions ) ,
keyboard : new contextual . KeyboardView ( viewOptions )
} ) ;
contextual . regionViews . push ( new contextual . RegionView (
$ . extend ( { el : $region , model : model } , options ) )
) ;
2015-09-04 13:20:09 -07:00
// Add the model to the collection. This must happen after the views have
// been associated with it, otherwise collection change event handlers can't
2015-08-17 17:00:26 -07:00
// trigger the model change event handler in its views.
contextual . collection . add ( model ) ;
// Let other JavaScript react to the adding of a new contextual link.
$ ( document ) . trigger ( 'drupalContextualLinkAdded' , {
$el : $contextual ,
$region : $region ,
model : model
} ) ;
// Fix visual collisions between contextual link triggers.
adjustIfNestedAndOverlapping ( $contextual ) ;
}
/ * *
* Determines if a contextual link is nested & overlapping , if so : adjusts it .
*
* This only deals with two levels of nesting ; deeper levels are not touched .
*
* @ param { jQuery } $contextual
* A contextual links placeholder DOM element , containing the actual
* contextual links as rendered by the server .
* /
function adjustIfNestedAndOverlapping ( $contextual ) {
var $contextuals = $contextual
// @todo confirm that .closest() is not sufficient
. parents ( '.contextual-region' ) . eq ( - 1 )
. find ( '.contextual' ) ;
// Early-return when there's no nesting.
if ( $contextuals . length === 1 ) {
return ;
}
// If the two contextual links overlap, then we move the second one.
var firstTop = $contextuals . eq ( 0 ) . offset ( ) . top ;
var secondTop = $contextuals . eq ( 1 ) . offset ( ) . top ;
if ( firstTop === secondTop ) {
var $nestedContextual = $contextuals . eq ( 1 ) ;
// Retrieve height of nested contextual link.
var height = 0 ;
var $trigger = $nestedContextual . find ( '.trigger' ) ;
// Elements with the .visually-hidden class have no dimensions, so this
// class must be temporarily removed to the calculate the height.
$trigger . removeClass ( 'visually-hidden' ) ;
height = $nestedContextual . height ( ) ;
$trigger . addClass ( 'visually-hidden' ) ;
// Adjust nested contextual link's position.
$nestedContextual . css ( { top : $nestedContextual . position ( ) . top + height } ) ;
}
}
/ * *
* Attaches outline behavior for regions associated with contextual links .
*
* Events
* Contextual triggers an event that can be used by other scripts .
* - drupalContextualLinkAdded : Triggered when a contextual link is added .
*
* @ type { Drupal ~ behavior }
2015-09-04 13:20:09 -07:00
*
* @ prop { Drupal ~ behaviorAttach } attach
* Attaches the outline behavior to the right context .
2015-08-17 17:00:26 -07:00
* /
Drupal . behaviors . contextual = {
attach : function ( context ) {
var $context = $ ( context ) ;
// Find all contextual links placeholders, if any.
var $placeholders = $context . find ( '[data-contextual-id]' ) . once ( 'contextual-render' ) ;
if ( $placeholders . length === 0 ) {
return ;
}
// Collect the IDs for all contextual links placeholders.
var ids = [ ] ;
$placeholders . each ( function ( ) {
ids . push ( $ ( this ) . attr ( 'data-contextual-id' ) ) ;
} ) ;
// Update all contextual links placeholders whose HTML is cached.
var uncachedIDs = _ . filter ( ids , function initIfCached ( contextualID ) {
var html = storage . getItem ( 'Drupal.contextual.' + contextualID ) ;
if ( html !== null ) {
// Initialize after the current execution cycle, to make the AJAX
// request for retrieving the uncached contextual links as soon as
2015-09-04 13:20:09 -07:00
// possible, but also to ensure that other Drupal behaviors have had
// the chance to set up an event listener on the Backbone collection
2015-08-17 17:00:26 -07:00
// Drupal.contextual.collection.
window . setTimeout ( function ( ) {
initContextual ( $context . find ( '[data-contextual-id="' + contextualID + '"]' ) , html ) ;
} ) ;
return false ;
}
return true ;
} ) ;
2015-09-04 13:20:09 -07:00
// Perform an AJAX request to let the server render the contextual links
// for each of the placeholders.
2015-08-17 17:00:26 -07:00
if ( uncachedIDs . length > 0 ) {
$ . ajax ( {
url : Drupal . url ( 'contextual/render' ) ,
type : 'POST' ,
data : { 'ids[]' : uncachedIDs } ,
dataType : 'json' ,
success : function ( results ) {
_ . each ( results , function ( html , contextualID ) {
// Store the metadata.
storage . setItem ( 'Drupal.contextual.' + contextualID , html ) ;
2015-09-04 13:20:09 -07:00
// If the rendered contextual links are empty, then the current
// user does not have permission to access the associated links:
// don't render anything.
2015-08-17 17:00:26 -07:00
if ( html . length > 0 ) {
2015-09-04 13:20:09 -07:00
// Update the placeholders to contain its rendered contextual
// links. Usually there will only be one placeholder, but it's
// possible for multiple identical placeholders exist on the
// page (probably because the same content appears more than
// once).
2015-08-17 17:00:26 -07:00
$placeholders = $context . find ( '[data-contextual-id="' + contextualID + '"]' ) ;
// Initialize the contextual links.
for ( var i = 0 ; i < $placeholders . length ; i ++ ) {
initContextual ( $placeholders . eq ( i ) , html ) ;
}
}
} ) ;
}
} ) ;
}
}
} ;
/ * *
2015-09-04 13:20:09 -07:00
* Namespace for contextual related functionality .
*
2015-08-17 17:00:26 -07:00
* @ namespace
* /
Drupal . contextual = {
/ * *
* The { @ link Drupal . contextual . View } instances associated with each list
* element of contextual links .
*
* @ type { Array }
* /
views : [ ] ,
/ * *
2015-09-04 13:20:09 -07:00
* The { @ link Drupal . contextual . RegionView } instances associated with each
* contextual region element .
2015-08-17 17:00:26 -07:00
*
* @ type { Array }
* /
regionViews : [ ]
} ;
/ * *
* A Backbone . Collection of { @ link Drupal . contextual . StateModel } instances .
*
* @ type { Backbone . Collection }
* /
Drupal . contextual . collection = new Backbone . Collection ( [ ] , { model : Drupal . contextual . StateModel } ) ;
/ * *
* A trigger is an interactive element often bound to a click handler .
*
* @ return { string }
* A string representing a DOM fragment .
* /
Drupal . theme . contextualTrigger = function ( ) {
return '<button class="trigger visually-hidden focusable" type="button"></button>' ;
} ;
} ) ( jQuery , Drupal , drupalSettings , _ , Backbone , window . JSON , window . sessionStorage ) ;