drupal/core/modules/contextual/contextual.js

202 lines
6.0 KiB
JavaScript

/**
* @file
* Attaches behaviors for the Contextual module.
*/
(function ($, Drupal) {
"use strict";
var contextuals = [];
/**
* Attaches outline behavior for regions associated with contextual links.
*/
Drupal.behaviors.contextual = {
attach: function (context) {
$('ul.contextual-links', context).once('contextual', function () {
var $this = $(this);
var contextual = new Drupal.contextual($this, $this.closest('.contextual-region'));
contextuals.push(contextual);
$this.data('drupal-contextual', contextual);
});
// Bind to edit mode changes.
$('body').once('contextual', function () {
$(document).on('drupalEditModeChanged.contextual', toggleEditMode);
});
}
};
/**
* Contextual links object.
*/
Drupal.contextual = function($links, $region) {
this.$links = $links;
this.$region = $region;
this.init();
};
/**
* Initiates a contextual links object.
*/
Drupal.contextual.prototype.init = function() {
// Wrap the links to provide positioning and behavior attachment context.
this.$wrapper = $(Drupal.theme.contextualWrapper())
.insertBefore(this.$links)
.append(this.$links);
// Mark the links as hidden. Use aria-role form so that the number of items
// in the list is spoken.
this.$links
.attr({
'hidden': 'hidden',
'role': 'form'
});
// Create and append the contextual links trigger.
var action = Drupal.t('Open');
var parentBlock = this.$region.find('h2').first().text();
this.$trigger = $(Drupal.theme.contextualTrigger())
.text(Drupal.t('@action @parent configuration options', {'@action': action, '@parent': parentBlock}))
// Set the aria-pressed state.
.attr('aria-pressed', false)
.prependTo(this.$wrapper);
// The trigger behaviors are never detached or mutated.
this.$region
.on('click.contextual', '.contextual .trigger', $.proxy(this.triggerClickHandler, this))
.on('mouseleave.contextual', '.contextual', {show: false}, $.proxy(this.triggerLeaveHandler, this));
// Attach highlight behaviors.
this.attachHighlightBehaviors();
};
/**
* Attaches highlight-on-mouseenter behaviors.
*/
Drupal.contextual.prototype.attachHighlightBehaviors = function () {
// Bind behaviors through delegation.
var highlightRegion = $.proxy(this.highlightRegion, this);
this.$region
.on('mouseenter.contextual.highlight', {highlight: true}, highlightRegion)
.on('mouseleave.contextual.highlight', {highlight: false}, highlightRegion)
.on('click.contextual.highlight', '.contextual-links a', {highlight: false}, highlightRegion)
.on('focus.contextual.highlight', '.contextual-links a, .contextual .trigger', {highlight: true}, highlightRegion)
.on('blur.contextual.highlight', '.contextual-links a, .contextual .trigger', {highlight: false}, highlightRegion);
};
/**
* Detaches unhighlight-on-mouseleave behaviors.
*/
Drupal.contextual.prototype.detachHighlightBehaviors = function () {
this.$region.off('.contextual.highlight');
};
/**
* Toggles the highlighting of a contextual region.
*
* @param {Object} event
* jQuery Event object.
*/
Drupal.contextual.prototype.highlightRegion = function(event) {
// Set up a timeout to delay the dismissal of the region highlight state.
if (!event.data.highlight && this.timer === undefined) {
return this.timer = window.setTimeout($.proxy($.fn.trigger, $(event.target), 'mouseleave.contextual'), 100);
}
// Clear the timeout to prevent an infinite loop of mouseleave being
// triggered.
if (this.timer) {
window.clearTimeout(this.timer);
delete this.timer;
}
// Toggle active state of the contextual region based on the highlight value.
this.$region.toggleClass('contextual-region-active', event.data.highlight);
// Hide the links if the contextual region is inactive.
var state = this.$region.hasClass('contextual-region-active');
if (!state) {
this.showLinks(state);
}
};
/**
* Handles click on the contextual links trigger.
*
* @param {Object} event
* jQuery Event object.
*/
Drupal.contextual.prototype.triggerClickHandler = function (event) {
event.preventDefault();
this.showLinks();
};
/**
* Handles mouseleave on the contextual links trigger.
*
* @param {Object} event
* jQuery Event object.
*/
Drupal.contextual.prototype.triggerLeaveHandler = function (event) {
var show = event && event.data && event.data.show;
this.showLinks(show);
};
/**
* Toggles the active state of the contextual links.
*
* @param {Boolean} show
* (optional) True if the links should be shown. False is the links should be
* hidden.
*/
Drupal.contextual.prototype.showLinks = function(show) {
this.$wrapper.toggleClass('contextual-links-active', show);
var isOpen = this.$wrapper.hasClass('contextual-links-active');
var action = (isOpen) ? Drupal.t('Close') : Drupal.t('Open');
var parentBlock = this.$region.find('h2').first().text();
this.$trigger
.text(Drupal.t('@action @parent configuration options', {'@action': action, '@parent': parentBlock}))
// Set the aria-pressed state.
.attr('aria-pressed', isOpen);
// Mark the links as hidden if they are.
if (isOpen) {
this.$links.removeAttr('hidden');
}
else {
this.$links.attr('hidden', 'hidden');
}
};
/**
* Shows or hides all pencil icons and corresponding contextual regions.
*/
function toggleEditMode (event, data) {
for (var i = contextuals.length - 1; i >= 0; i--) {
contextuals[i][(data.status) ? 'detachHighlightBehaviors' : 'attachHighlightBehaviors']();
contextuals[i].$region.toggleClass('contextual-region-active', data.status);
}
}
/**
* Wraps contextual links.
*
* @return {String}
* A string representing a DOM fragment.
*/
Drupal.theme.contextualWrapper = function () {
return '<div class="contextual" />';
};
/**
* 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 element-invisible element-focusable" type="button"></button>';
};
})(jQuery, Drupal);