NEW Extend the ssui.button with alternate appearances.

Reusable feature for making CMS buttons that respond to the current
contextual state with an appearance change.

Provides capability to specify initial state, alternate icon and
alternate text via data attributes or options (PHP or JS), and to hook
up into events triggered when the state is alternating.

This is used by the follow-up cms action buttons cleanup work.
This commit is contained in:
Mateusz Uzdowski 2012-11-06 16:14:30 +13:00
parent 15a687f1e7
commit dbbcd08d8f
2 changed files with 261 additions and 4 deletions

View File

@ -1,18 +1,115 @@
(function($) {
/**
* Allows icon definition via HTML5 data attrs for easier handling in PHP
* Allows icon definition via HTML5 data attrs for easier handling in PHP.
*
* Adds an alternative appearance so we can toggle back and forth between them
* and register event handlers to add custom styling and behaviour. Example use
* is in the CMS with the saving buttons - depending on the page's state one of
* them will either say "Save draft" or "Saved", and will have different colour.
*/
$.widget('ssui.button', $.ui.button, {
options: {
alternate: {
icon: null,
text: null
},
showingAlternate: false
},
/**
* Switch between the alternate appearances.
*/
toggleAlternate: function() {
if (this._trigger('ontogglealternate')===false) return;
// Only switch to alternate if it has been enabled through options.
if (!this.options.alternate.icon && !this.options.alternate.text) return;
this.options.showingAlternate = !this.options.showingAlternate;
this.refresh();
},
/**
* Adjust the appearance to fit with the current settings.
*/
_refreshAlternate: function() {
this._trigger('beforerefreshalternate');
// Only switch to alternate if it has been enabled through options.
if (!this.options.alternate.icon && !this.options.alternate.text) return;
if (this.options.showingAlternate) {
this.element.find('.ui-button-icon-primary').hide();
this.element.find('.ui-button-text').hide();
this.element.find('.ui-button-icon-alternate').show();
this.element.find('.ui-button-text-alternate').show();
}
else {
this.element.find('.ui-button-icon-primary').show();
this.element.find('.ui-button-text').show();
this.element.find('.ui-button-icon-alternate').hide();
this.element.find('.ui-button-text-alternate').hide();
}
this._trigger('afterrefreshalternate');
},
/**
* Construct button - pulls in options from data attributes.
* Injects new elements for alternate appearance (if requested via options).
*/
_resetButton: function() {
var iconPrimary = this.element.data('iconPrimary') ? this.element.data('iconPrimary') : this.element.data('icon'),
iconSecondary = this.element.data('iconSecondary');
var iconPrimary = this.element.data('icon-primary'),
iconSecondary = this.element.data('icon-secondary');
if (!iconPrimary) iconPrimary = this.element.data('icon');
// TODO Move prefix out of this method, without requriing it for every icon definition in a data attr
if(iconPrimary) this.options.icons.primary = 'btn-icon-' + iconPrimary;
if(iconSecondary) this.options.icons.secondary = 'btn-icon-' + iconSecondary;
$.ui.button.prototype._resetButton.call(this);
// Pull options from data attributes. Overriden by explicit options given on widget creation.
if (!this.options.alternate.text) {
this.options.alternate.text = this.element.data('text-alternate');
}
if (!this.options.alternate.icon) {
this.options.alternate.icon = this.element.data('icon-alternate');
}
if (!this.options.showingAlternate) {
this.options.showingAlternate = this.element.hasClass('ss-ui-alternate');
}
// Create missing elements.
if (this.options.alternate.text) {
this.buttonElement.append(
"<span class='ui-button-text-alternate ui-button-text'>" + this.options.alternate.text + "</span>"
);
}
if (this.options.alternate.icon) {
this.buttonElement.append(
"<span class='ui-button-icon-alternate ui-button-icon-primary ui-icon btn-icon-"
+ this.options.alternate.icon + "'></span>"
);
}
this._refreshAlternate();
},
refresh: function() {
$.ui.button.prototype.refresh.call(this);
this._refreshAlternate();
},
destroy: function() {
this.element.find('.ui-button-text-alternate').remove();
this.element.find('.ui-button-icon-alternate').remove();
$.ui.button.prototype.destroy.call( this );
},
});
/**

View File

@ -0,0 +1,160 @@
# How to implement an alternating button #
## Introduction ##
*Save* and *Save & publish* buttons alternate their appearance to reflect the state of the underlying `SiteTree` object.
This is based on a `ssui.button` extension available in `ssui.core.js`.
The button can be configured via the data attributes in the backend, or through jQuery UI initialisation options. The
state can be toggled from the backend (again through data attributes), and can also be easily toggled or set on the
frontend.
This how-to will walk you through creation of a "Clean-up" button with two appearances:
* active: "Clean-up now" green constructive button if the actions can be performed
* netural: "Cleaned" default button if the action does not need to be done
The controller code that goes with this example is listed in [Extend CMS Interface](../reference/extend-cms-interface).
## Backend support ##
First create and configure the action button with alternate state on a page type. The button comes with the default
state already, so you just need to add the alternate state using two data additional attributes:
* `data-icon-alternate`: icon to be shown when the button is in the alternate state
* `data-text-alternate`: likewise for text.
Here is the configuration code for the button:
:::php
public function getCMSActions() {
$fields = parent::getCMSActions();
$fields->fieldByName('MajorActions')->push(
$cleanupAction = FormAction::create('cleanup', 'Cleaned')
// Set up an icon for the neutral state that will use the default text.
->setAttribute('data-icon', 'accept')
// Initialise the alternate constructive state.
->setAttribute('data-icon-alternate', 'addpage')
->setAttribute('data-text-alternate', 'Clean-up now')
);
return $fields;
}
You can control the state of the button from the backend by applying `ss-ui-alternate` class to the `FormAction`. To
simplify our example, let's assume the button state is controlled on the backend only, but you'd usually be better off
adjusting the state in the frontend to give the user the benefit of immediate feedback. This technique might still be
used for initialisation though.
Here we initialise the button based on the backend check, and assume that the button will only update after page reload
(or on CMS action).
:::php
public function getCMSActions() {
// ...
if ($this->needsCleaning()) {
// Will initialise the button into alternate state.
$cleanupAction->addExtraClass('ss-ui-alternate');
}
// ...
}
## Frontend support ##
As with the *Save* and *Save & publish* buttons, you might want to add some scripted reactions to user actions on the
frontend. You can affect the state of the button through the jQuery UI calls.
First of all, you can toggle the state of the button - execute this code in the browser's console to see how it works.
:::js
jQuery('.cms-edit-form .Actions #Form_EditForm_action_cleanup').button('toggleAlternate');
Another, more useful, scenario is to check the current state.
:::js
jQuery('.cms-edit-form .Actions #Form_EditForm_action_cleanup').button('option', 'showingAlternate');
You can also force the button into a specific state by using UI options.
:::js
jQuery('.cms-edit-form .Actions #Form_EditForm_action_cleanup').button({showingAlternate: true});
This will allow you to react to user actions in the CMS and give immediate feedback. Here is an example taken from the
CMS core that tracks the changes to the input fields and reacts by enabling the *Save* and *Save & publish* buttons
(changetracker will automatically add `changed` class to the form if a modification is detected).
:::js
/**
* Enable save buttons upon detecting changes to content.
* "changed" class is added by jQuery.changetracker.
*/
$('.cms-edit-form .changed').entwine({
// This will execute when the class is added to the element.
onmatch: function(e) {
var form = this.closest('.cms-edit-form');
form.find('#Form_EditForm_action_save').button({showingAlternate: true});
form.find('#Form_EditForm_action_publish').button({showingAlternate: true});
this._super(e);
},
// Entwine requires us to define this, even if we don't use it.
onunmatch: function(e) {
this._super(e);
}
});
## Frontend hooks ##
`ssui.button` defines several additional events so that you can extend the code with your own behaviours. For example
this is used in the CMS to style the buttons. Three events are available:
* `ontogglealternate`: invoked when the `toggleAlternate` is called. Return `false` to prevent the toggling.
* `beforerefreshalternate`: invoked before the alternate-specific rendering takes place, including the button
initialisation.
* `afterrefreshalternate`: invoked after the rendering has been done, including on init. Good place to add styling
extras.
Continuing our example let's add a "constructive" style to our *Clean-up* button. First you need to be able to add
custom JS code into the CMS. You can do this by adding a new source file, here
`mysite/javascript/CMSMain.CustomActionsExtension.js`, and requiring it from the config.
:::ss
LeftAndMain::require_javascript('mysite/javascript/CMSMain.CustomActionsExtension.js');
You can now add the styling in response to `afterrefreshalternate` event. Let's use entwine to avoid accidental memory
leaks. The only complex part here is how the entwine handle is constructed. `onbuttonafterrefreshalternate` can be
disassembled into:
* `on` signifies the entiwne event handler
* `button` is jQuery UI widget name
* `afterrefreshalternate`: the event from ssui.button to react to.
Here is the entire handler put together. You don't need to add any separate initialisation code, this will handle all
cases.
:::js
(function($) {
$.entwine('mysite', function($){
$('.cms-edit-form .Actions #Form_EditForm_action_cleanup').entwine({
/**
* onafterrefreshalternate is SS-specific jQuery UI hook that is executed
* every time the button is rendered (including on initialisation).
*/
onbuttonafterrefreshalternate: function() {
if (this.button('option', 'showingAlternate')) {
this.addClass('ss-ui-action-constructive');
}
else {
this.removeClass('ss-ui-action-constructive');
}
}
});
});
}(jQuery));
## Summary ##
The code presented gives you a fully functioning alternating button, similar to the defaults that come with the the CMS.
These alternating buttons can be used to give user the advantage of visual feedback upon his actions.