Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176
This commit is contained in:
commit
9921556621
13277 changed files with 1459781 additions and 0 deletions
516
core/modules/quickedit/js/views/EntityToolbarView.js
Normal file
516
core/modules/quickedit/js/views/EntityToolbarView.js
Normal file
|
@ -0,0 +1,516 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View that provides an entity level toolbar.
|
||||
*/
|
||||
|
||||
(function ($, _, Backbone, Drupal, debounce) {
|
||||
|
||||
"use strict";
|
||||
|
||||
Drupal.quickedit.EntityToolbarView = Backbone.View.extend(/** @lends Drupal.quickedit.EntityToolbarView# */{
|
||||
|
||||
/**
|
||||
* @type {jQuery}
|
||||
*/
|
||||
_fieldToolbarRoot: null,
|
||||
|
||||
/**
|
||||
* @return {object}
|
||||
*/
|
||||
events: function () {
|
||||
var map = {
|
||||
'click button.action-save': 'onClickSave',
|
||||
'click button.action-cancel': 'onClickCancel',
|
||||
'mouseenter': 'onMouseenter'
|
||||
};
|
||||
return map;
|
||||
},
|
||||
|
||||
/**
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {Drupal.quickedit.AppModel} options.appModel
|
||||
*/
|
||||
initialize: function (options) {
|
||||
var that = this;
|
||||
this.appModel = options.appModel;
|
||||
this.$entity = $(this.model.get('el'));
|
||||
|
||||
// Rerender whenever the entity state changes.
|
||||
this.listenTo(this.model, 'change:isActive change:isDirty change:state', this.render);
|
||||
// Also rerender whenever a different field is highlighted or activated.
|
||||
this.listenTo(this.appModel, 'change:highlightedField change:activeField', this.render);
|
||||
// Rerender when a field of the entity changes state.
|
||||
this.listenTo(this.model.get('fields'), 'change:state', this.fieldStateChange);
|
||||
|
||||
// Reposition the entity toolbar as the viewport and the position within
|
||||
// the viewport changes.
|
||||
$(window).on('resize.quickedit scroll.quickedit', debounce($.proxy(this.windowChangeHandler, this), 150));
|
||||
|
||||
// Adjust the fence placement within which the entity toolbar may be
|
||||
// positioned.
|
||||
$(document).on('drupalViewportOffsetChange.quickedit', function (event, offsets) {
|
||||
if (that.$fence) {
|
||||
that.$fence.css(offsets);
|
||||
}
|
||||
});
|
||||
|
||||
// Set the entity toolbar DOM element as the el for this view.
|
||||
var $toolbar = this.buildToolbarEl();
|
||||
this.setElement($toolbar);
|
||||
this._fieldToolbarRoot = $toolbar.find('.quickedit-toolbar-field').get(0);
|
||||
|
||||
// Initial render.
|
||||
this.render();
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @return {Drupal.quickedit.EntityToolbarView}
|
||||
*/
|
||||
render: function () {
|
||||
if (this.model.get('isActive')) {
|
||||
// If the toolbar container doesn't exist, create it.
|
||||
var $body = $('body');
|
||||
if ($body.children('#quickedit-entity-toolbar').length === 0) {
|
||||
$body.append(this.$el);
|
||||
}
|
||||
// The fence will define a area on the screen that the entity toolbar
|
||||
// will be position within.
|
||||
if ($body.children('#quickedit-toolbar-fence').length === 0) {
|
||||
this.$fence = $(Drupal.theme('quickeditEntityToolbarFence'))
|
||||
.css(Drupal.displace())
|
||||
.appendTo($body);
|
||||
}
|
||||
// Adds the entity title to the toolbar.
|
||||
this.label();
|
||||
|
||||
// Show the save and cancel buttons.
|
||||
this.show('ops');
|
||||
// If render is being called and the toolbar is already visible, just
|
||||
// reposition it.
|
||||
this.position();
|
||||
}
|
||||
|
||||
// The save button text and state varies with the state of the entity
|
||||
// model.
|
||||
var $button = this.$el.find('.quickedit-button.action-save');
|
||||
var isDirty = this.model.get('isDirty');
|
||||
// Adjust the save button according to the state of the model.
|
||||
switch (this.model.get('state')) {
|
||||
// Quick editing is active, but no field is being edited.
|
||||
case 'opened':
|
||||
// The saving throbber is not managed by AJAX system. The
|
||||
// EntityToolbarView manages this visual element.
|
||||
$button
|
||||
.removeClass('action-saving icon-throbber icon-end')
|
||||
.text(Drupal.t('Save'))
|
||||
.removeAttr('disabled')
|
||||
.attr('aria-hidden', !isDirty);
|
||||
break;
|
||||
|
||||
// The changes to the fields of the entity are being committed.
|
||||
case 'committing':
|
||||
$button
|
||||
.addClass('action-saving icon-throbber icon-end')
|
||||
.text(Drupal.t('Saving'))
|
||||
.attr('disabled', 'disabled');
|
||||
break;
|
||||
|
||||
default:
|
||||
$button.attr('aria-hidden', true);
|
||||
break;
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
remove: function () {
|
||||
// Remove additional DOM elements controlled by this View.
|
||||
this.$fence.remove();
|
||||
|
||||
// Stop listening to additional events.
|
||||
$(window).off('resize.quickedit scroll.quickedit');
|
||||
$(document).off('drupalViewportOffsetChange.quickedit');
|
||||
|
||||
Backbone.View.prototype.remove.call(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Repositions the entity toolbar on window scroll and resize.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
*/
|
||||
windowChangeHandler: function (event) {
|
||||
this.position();
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines the actions to take given a change of state.
|
||||
*
|
||||
* @param {Drupal.quickedit.FieldModel} model
|
||||
* @param {string} state
|
||||
* The state of the associated field. One of
|
||||
* {@link Drupal.quickedit.FieldModel.states}.
|
||||
*/
|
||||
fieldStateChange: function (model, state) {
|
||||
switch (state) {
|
||||
case 'active':
|
||||
this.render();
|
||||
break;
|
||||
|
||||
case 'invalid':
|
||||
this.render();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Uses the jQuery.ui.position() method to position the entity toolbar.
|
||||
*
|
||||
* @param {HTMLElement} [element]
|
||||
* The element against which the entity toolbar is positioned.
|
||||
*/
|
||||
position: function (element) {
|
||||
clearTimeout(this.timer);
|
||||
|
||||
var that = this;
|
||||
// Vary the edge of the positioning according to the direction of language
|
||||
// in the document.
|
||||
var edge = (document.documentElement.dir === 'rtl') ? 'right' : 'left';
|
||||
// A time unit to wait until the entity toolbar is repositioned.
|
||||
var delay = 0;
|
||||
// Determines what check in the series of checks below should be
|
||||
// evaluated.
|
||||
var check = 0;
|
||||
// When positioned against an active field that has padding, we should
|
||||
// ignore that padding when positioning the toolbar, to not unnecessarily
|
||||
// move the toolbar horizontally, which feels annoying.
|
||||
var horizontalPadding = 0;
|
||||
var of;
|
||||
var activeField;
|
||||
var highlightedField;
|
||||
// There are several elements in the page that the entity toolbar might be
|
||||
// positioned against. They are considered below in a priority order.
|
||||
do {
|
||||
switch (check) {
|
||||
case 0:
|
||||
// Position against a specific element.
|
||||
of = element;
|
||||
break;
|
||||
|
||||
case 1:
|
||||
// Position against a form container.
|
||||
activeField = Drupal.quickedit.app.model.get('activeField');
|
||||
of = activeField && activeField.editorView && activeField.editorView.$formContainer && activeField.editorView.$formContainer.find('.quickedit-form');
|
||||
break;
|
||||
|
||||
case 2:
|
||||
// Position against an active field.
|
||||
of = activeField && activeField.editorView && activeField.editorView.getEditedElement();
|
||||
if (activeField && activeField.editorView && activeField.editorView.getQuickEditUISettings().padding) {
|
||||
horizontalPadding = 5;
|
||||
}
|
||||
break;
|
||||
|
||||
case 3:
|
||||
// Position against a highlighted field.
|
||||
highlightedField = Drupal.quickedit.app.model.get('highlightedField');
|
||||
of = highlightedField && highlightedField.editorView && highlightedField.editorView.getEditedElement();
|
||||
delay = 250;
|
||||
break;
|
||||
|
||||
default:
|
||||
var fieldModels = this.model.get('fields').models;
|
||||
var topMostPosition = 1000000;
|
||||
var topMostField = null;
|
||||
// Position against the topmost field.
|
||||
for (var i = 0; i < fieldModels.length; i++) {
|
||||
var pos = fieldModels[i].get('el').getBoundingClientRect().top;
|
||||
if (pos < topMostPosition) {
|
||||
topMostPosition = pos;
|
||||
topMostField = fieldModels[i];
|
||||
}
|
||||
}
|
||||
of = topMostField.get('el');
|
||||
delay = 50;
|
||||
break;
|
||||
}
|
||||
// Prepare to check the next possible element to position against.
|
||||
check++;
|
||||
} while (!of);
|
||||
|
||||
/**
|
||||
* Refines the positioning algorithm of jquery.ui.position().
|
||||
*
|
||||
* Invoked as the 'using' callback of jquery.ui.position() in
|
||||
* positionToolbar().
|
||||
*
|
||||
* @param {*} view
|
||||
* @param {object} suggested
|
||||
* A hash of top and left values for the position that should be set. It
|
||||
* can be forwarded to .css() or .animate().
|
||||
* @param {object} info
|
||||
* The position and dimensions of both the 'my' element and the 'of'
|
||||
* elements, as well as calculations to their relative position. This
|
||||
* object contains the following properties:
|
||||
* @param {object} info.element
|
||||
* A hash that contains information about the HTML element that will be
|
||||
* positioned. Also known as the 'my' element.
|
||||
* @param {object} info.target
|
||||
* A hash that contains information about the HTML element that the
|
||||
* 'my' element will be positioned against. Also known as the 'of'
|
||||
* element.
|
||||
*/
|
||||
function refinePosition(view, suggested, info) {
|
||||
// Determine if the pointer should be on the top or bottom.
|
||||
var isBelow = suggested.top > info.target.top;
|
||||
info.element.element.toggleClass('quickedit-toolbar-pointer-top', isBelow);
|
||||
// Don't position the toolbar past the first or last editable field if
|
||||
// the entity is the target.
|
||||
if (view.$entity[0] === info.target.element[0]) {
|
||||
// Get the first or last field according to whether the toolbar is
|
||||
// above or below the entity.
|
||||
var $field = view.$entity.find('.quickedit-editable').eq((isBelow) ? -1 : 0);
|
||||
if ($field.length > 0) {
|
||||
suggested.top = (isBelow) ? ($field.offset().top + $field.outerHeight(true)) : $field.offset().top - info.element.element.outerHeight(true);
|
||||
}
|
||||
}
|
||||
// Don't let the toolbar go outside the fence.
|
||||
var fenceTop = view.$fence.offset().top;
|
||||
var fenceHeight = view.$fence.height();
|
||||
var toolbarHeight = info.element.element.outerHeight(true);
|
||||
if (suggested.top < fenceTop) {
|
||||
suggested.top = fenceTop;
|
||||
}
|
||||
else if ((suggested.top + toolbarHeight) > (fenceTop + fenceHeight)) {
|
||||
suggested.top = fenceTop + fenceHeight - toolbarHeight;
|
||||
}
|
||||
// Position the toolbar.
|
||||
info.element.element.css({
|
||||
left: Math.floor(suggested.left),
|
||||
top: Math.floor(suggested.top)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the jquery.ui.position() method on the $el of this view.
|
||||
*/
|
||||
function positionToolbar() {
|
||||
that.$el
|
||||
.position({
|
||||
my: edge + ' bottom',
|
||||
// Move the toolbar 1px towards the start edge of the 'of' element,
|
||||
// plus any horizontal padding that may have been added to the
|
||||
// element that is being added, to prevent unwanted horizontal
|
||||
// movement.
|
||||
at: edge + '+' + (1 + horizontalPadding) + ' top',
|
||||
of: of,
|
||||
collision: 'flipfit',
|
||||
using: refinePosition.bind(null, that),
|
||||
within: that.$fence
|
||||
})
|
||||
// Resize the toolbar to match the dimensions of the field, up to a
|
||||
// maximum width that is equal to 90% of the field's width.
|
||||
.css({
|
||||
'max-width': (document.documentElement.clientWidth < 450) ? document.documentElement.clientWidth : 450,
|
||||
// Set a minimum width of 240px for the entity toolbar, or the width
|
||||
// of the client if it is less than 240px, so that the toolbar
|
||||
// never folds up into a squashed and jumbled mess.
|
||||
'min-width': (document.documentElement.clientWidth < 240) ? document.documentElement.clientWidth : 240,
|
||||
'width': '100%'
|
||||
});
|
||||
}
|
||||
|
||||
// Uses the jQuery.ui.position() method. Use a timeout to move the toolbar
|
||||
// only after the user has focused on an editable for 250ms. This prevents
|
||||
// the toolbar from jumping around the screen.
|
||||
this.timer = setTimeout(function () {
|
||||
// Render the position in the next execution cycle, so that animations
|
||||
// on the field have time to process. This is not strictly speaking, a
|
||||
// guarantee that all animations will be finished, but it's a simple
|
||||
// way to get better positioning without too much additional code.
|
||||
_.defer(positionToolbar);
|
||||
}, delay);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the model state to 'saving' when the save button is clicked.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
*/
|
||||
onClickSave: function (event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
// Save the model.
|
||||
this.model.set('state', 'committing');
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the model state to candidate when the cancel button is clicked.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
*/
|
||||
onClickCancel: function (event) {
|
||||
event.preventDefault();
|
||||
this.model.set('state', 'deactivating');
|
||||
},
|
||||
|
||||
/**
|
||||
* Clears the timeout that will eventually reposition the entity toolbar.
|
||||
*
|
||||
* Without this, it may reposition itself, away from the user's cursor!
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
*/
|
||||
onMouseenter: function (event) {
|
||||
clearTimeout(this.timer);
|
||||
},
|
||||
|
||||
/**
|
||||
* Builds the entity toolbar HTML; attaches to DOM; sets starting position.
|
||||
*
|
||||
* @return {jQuery}
|
||||
*/
|
||||
buildToolbarEl: function () {
|
||||
var $toolbar = $(Drupal.theme('quickeditEntityToolbar', {
|
||||
id: 'quickedit-entity-toolbar'
|
||||
}));
|
||||
|
||||
$toolbar
|
||||
.find('.quickedit-toolbar-entity')
|
||||
// Append the "ops" toolgroup into the toolbar.
|
||||
.prepend(Drupal.theme('quickeditToolgroup', {
|
||||
classes: ['ops'],
|
||||
buttons: [
|
||||
{
|
||||
label: Drupal.t('Save'),
|
||||
type: 'submit',
|
||||
classes: 'action-save quickedit-button icon',
|
||||
attributes: {
|
||||
'aria-hidden': true
|
||||
}
|
||||
},
|
||||
{
|
||||
label: Drupal.t('Close'),
|
||||
classes: 'action-cancel quickedit-button icon icon-close icon-only'
|
||||
}
|
||||
]
|
||||
}));
|
||||
|
||||
// Give the toolbar a sensible starting position so that it doesn't
|
||||
// animate on to the screen from a far off corner.
|
||||
$toolbar
|
||||
.css({
|
||||
left: this.$entity.offset().left,
|
||||
top: this.$entity.offset().top
|
||||
});
|
||||
|
||||
return $toolbar;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the DOM element that fields will attach their toolbars to.
|
||||
*
|
||||
* @return {jQuery}
|
||||
* The DOM element that fields will attach their toolbars to.
|
||||
*/
|
||||
getToolbarRoot: function () {
|
||||
return this._fieldToolbarRoot;
|
||||
},
|
||||
|
||||
/**
|
||||
* Generates a state-dependent label for the entity toolbar.
|
||||
*/
|
||||
label: function () {
|
||||
// The entity label.
|
||||
var label = '';
|
||||
var entityLabel = this.model.get('label');
|
||||
|
||||
// Label of an active field, if it exists.
|
||||
var activeField = Drupal.quickedit.app.model.get('activeField');
|
||||
var activeFieldLabel = activeField && activeField.get('metadata').label;
|
||||
// Label of a highlighted field, if it exists.
|
||||
var highlightedField = Drupal.quickedit.app.model.get('highlightedField');
|
||||
var highlightedFieldLabel = highlightedField && highlightedField.get('metadata').label;
|
||||
// The label is constructed in a priority order.
|
||||
if (activeFieldLabel) {
|
||||
label = Drupal.theme('quickeditEntityToolbarLabel', {
|
||||
entityLabel: entityLabel,
|
||||
fieldLabel: activeFieldLabel
|
||||
});
|
||||
}
|
||||
else if (highlightedFieldLabel) {
|
||||
label = Drupal.theme('quickeditEntityToolbarLabel', {
|
||||
entityLabel: entityLabel,
|
||||
fieldLabel: highlightedFieldLabel
|
||||
});
|
||||
}
|
||||
else {
|
||||
label = entityLabel;
|
||||
}
|
||||
|
||||
this.$el
|
||||
.find('.quickedit-toolbar-label')
|
||||
.html(label);
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds classes to a toolgroup.
|
||||
*
|
||||
* @param {string} toolgroup
|
||||
* A toolgroup name.
|
||||
* @param {string} classes
|
||||
* A string of space-delimited class names that will be applied to the
|
||||
* wrapping element of the toolbar group.
|
||||
*/
|
||||
addClass: function (toolgroup, classes) {
|
||||
this._find(toolgroup).addClass(classes);
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes classes from a toolgroup.
|
||||
*
|
||||
* @param {string} toolgroup
|
||||
* A toolgroup name.
|
||||
* @param {string} classes
|
||||
* A string of space-delimited class names that will be removed from the
|
||||
* wrapping element of the toolbar group.
|
||||
*/
|
||||
removeClass: function (toolgroup, classes) {
|
||||
this._find(toolgroup).removeClass(classes);
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds a toolgroup.
|
||||
*
|
||||
* @param {string} toolgroup
|
||||
* A toolgroup name.
|
||||
*
|
||||
* @return {jQuery}
|
||||
* The toolgroup DOM element.
|
||||
*/
|
||||
_find: function (toolgroup) {
|
||||
return this.$el.find('.quickedit-toolbar .quickedit-toolgroup.' + toolgroup);
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows a toolgroup.
|
||||
*
|
||||
* @param {string} toolgroup
|
||||
* A toolgroup name.
|
||||
*/
|
||||
show: function (toolgroup) {
|
||||
this.$el.removeClass('quickedit-animate-invisible');
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
})(jQuery, _, Backbone, Drupal, Drupal.debounce);
|
Reference in a new issue