2015-08-17 17:00:26 -07:00
/ * *
* @ file
* A Backbone View that provides an entity level toolbar .
* /
( function ( $ , _ , Backbone , Drupal , debounce ) {
2015-10-21 21:44:50 -07:00
'use strict' ;
2015-08-17 17:00:26 -07:00
Drupal . quickedit . EntityToolbarView = Backbone . View . extend ( /** @lends Drupal.quickedit.EntityToolbarView# */ {
/ * *
* @ type { jQuery }
* /
_fieldToolbarRoot : null ,
/ * *
* @ return { object }
2015-09-04 13:20:09 -07:00
* A map of events .
2015-08-17 17:00:26 -07:00
* /
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
2015-09-04 13:20:09 -07:00
* Options to construct the view .
2015-08-17 17:00:26 -07:00
* @ param { Drupal . quickedit . AppModel } options . appModel
2015-09-04 13:20:09 -07:00
* A quickedit ` AppModel ` to use in the view .
2015-08-17 17:00:26 -07:00
* /
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 }
2015-09-04 13:20:09 -07:00
* The entity toolbar view .
2015-08-17 17:00:26 -07:00
* /
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
2015-09-04 13:20:09 -07:00
* The scroll or resize event .
2015-08-17 17:00:26 -07:00
* /
windowChangeHandler : function ( event ) {
this . position ( ) ;
} ,
/ * *
* Determines the actions to take given a change of state .
*
* @ param { Drupal . quickedit . FieldModel } model
2015-09-04 13:20:09 -07:00
* The ` FieldModel ` model .
2015-08-17 17:00:26 -07:00
* @ 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
2015-09-04 13:20:09 -07:00
* The view the positions will be calculated from .
2015-08-17 17:00:26 -07:00
* @ 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
2015-09-04 13:20:09 -07:00
* The click event .
2015-08-17 17:00:26 -07:00
* /
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
2015-09-04 13:20:09 -07:00
* The click event .
2015-08-17 17:00:26 -07:00
* /
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
2015-09-04 13:20:09 -07:00
* The mouse event .
2015-08-17 17:00:26 -07:00
* /
onMouseenter : function ( event ) {
clearTimeout ( this . timer ) ;
} ,
/ * *
* Builds the entity toolbar HTML ; attaches to DOM ; sets starting position .
*
* @ return { jQuery }
2015-09-04 13:20:09 -07:00
* The toolbar element .
2015-08-17 17:00:26 -07:00
* /
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 {
2015-09-04 13:20:09 -07:00
// @todo Add XSS regression test coverage in https://www.drupal.org/node/2547437
label = Drupal . checkPlain ( entityLabel ) ;
2015-08-17 17:00:26 -07:00
}
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 ) ;